Você recebeu uma lista de CEPs de clientes e precisa saber a cidade, o DDD e a região de cada um. Roda a consulta, e alguns voltam como "não encontrado", outros vêm sem rua, e o que o serviço chama de erro muda de um para outro. Eu sei como é: a lista é grande, e você não sabe se o problema está no CEP ou no seu código.
Neste artigo você vai consultar CEP pela AwesomeAPI, separar CEP inexistente de CEP inválido e de CEP de cidade, e juntar o resultado com a API do IBGE pelo código do município. O código em Python foi executado aqui. As consultas em Power Query foram montadas pela documentação oficial, e você testa no seu Power BI Desktop. É a mesma lógica do dashboard de consulta de CEP da Chora API. Bora lá.
Requisitos
- AwesomeAPI CEP (
cep.awesomeapi.com.br). Pública. Em todos os testes deste artigo, nenhuma chave foi necessária. - API de localidades e de malhas do IBGE (
servicodados.ibge.gov.br). Públicas, sem chave. - Para a versão Python: Python 3.10 ou mais novo, só com a biblioteca padrão.
- Para a versão Power Query: Power BI Desktop ou Excel com Power Query, conector Web com acesso Anônimo.
- Demo ao vivo: choraapi.com.br/dashboards/consulta-cep/ busca as mesmas APIs direto do navegador.
Válido em: novembro/2026.
O que cada API entrega
| Pergunta | API | O que devolve |
|---|---|---|
| Qual é o endereço, o DDD e a coordenada deste CEP? | AwesomeAPI | Logradouro, bairro, cidade, UF, DDD, latitude e longitude, e o código IBGE do município (city_ibge) |
| Em que região, UF e microrregião fica esse município? | IBGE, localidades | Nome, UF, região e regiões imediata e intermediária |
| Qual é o contorno desse município? | IBGE, malhas | A forma do município em GeoJSON, para desenhar o mapa |
A ligação entre as três é o código do município: ele sai da AwesomeAPI em city_ibge e entra no IBGE.
1 Consulte um CEP
A chamada é um GET com o CEP de 8 dígitos no fim:
curl -s "https://cep.awesomeapi.com.br/json/01001000"
{"cep":"01001000","address_type":"Praça","address_name":"da Sé","address":"Praça da Sé","state":"SP","district":"Sé","lat":"-23.5500806","lng":"-46.6340827","city":"São Paulo","city_ibge":"3550308","ddd":"11"}
Três coisas para guardar:
latelngvêm como texto, entre aspas. Converta antes de calcular qualquer coisa.city_ibgeé o código do município com 7 dígitos, também como texto:"3550308".- O hífen funciona, mesmo a documentação pedindo só os 8 números:
01001-000voltou 200 e o campocepveio sem o hífen. Como isso não está documentado, normalize você mesmo e não dependa dele.
Troque json por xml na URL e a resposta vem em XML.
2 404, 400 e CEP de cidade
A API tem três respostas que você precisa separar. Todas executadas em 07/11/2026:
GET /json/37925000 -> 200 {"cep":"37925000","address_type":"","address_name":"","address":"","state":"MG","district":"","lat":"-20.4742488","lng":"-45.9619861","city":"Piumhi","city_ibge":"3151503","ddd":"37"}
GET /json/99999999 -> 404 {"code":"not_found","message":"O CEP 99999999 nao foi encontrado"}
GET /json/1001000 -> 400 {"code":"invalid","message":"CEP invalido, tente: 00000000"}
- 200 com a rua vazia é um CEP de cidade, um CEP que identifica a cidade inteira, e não uma rua. Tem cidade, UF, DDD e coordenada da cidade, mas
address,address_name,address_typeedistrictvêm como texto vazio,"". - 404 é um CEP de formato válido que a base não tem.
- 400 é um CEP de formato inválido: menos ou mais de 8 dígitos, ou com letras (
1001000,010010000eabc12345voltaram o mesmo 400).
a mensagem do 400 sugere 00000000 como exemplo, e o 00000000 volta 404. E a mensagem do 404 muda se o CEP vier vazio ({"code":"not_found","message":"Not found"}). Não dependa do texto: use o código HTTP.
3 Padronize a consulta em Python
Este bloco e os dois seguintes formam um script só, executado em 07/11/2026. Primeiro, a função que baixa e a que consulta o CEP:
import gzip, json, re, time, urllib.error, urllib.request
from collections import Counter
from concurrent.futures import ThreadPoolExecutor
CEP_API = "https://cep.awesomeapi.com.br/json/"
IBGE_MUN = "https://servicodados.ibge.gov.br/api/v1/localidades/municipios/"
def baixar(url, tentativas=3):
"""Devolve (status, JSON). 400 e 404 voltam como resposta, não como exceção."""
for n in range(1, tentativas + 1):
try:
with urllib.request.urlopen(url, timeout=30) as r:
status, corpo, enc = r.status, r.read(), r.headers.get("Content-Encoding")
except urllib.error.HTTPError as e:
status, corpo, enc = e.code, e.read(), e.headers.get("Content-Encoding")
if status in (502, 503, 504) and n < tentativas:
time.sleep(3 * n)
continue
if enc == "gzip": # o IBGE manda gzip, e o urllib não descomprime sozinho
corpo = gzip.decompress(corpo)
return status, json.loads(corpo)
def numero(s): # a API manda lat e lng como texto
try:
v = float(s)
return v if v != 0 else None
except (TypeError, ValueError):
return None
def consultar_cep(texto):
cep = re.sub(r"\D", "", str(texto)) # aceita 01001-000 e 01.001-000
if len(cep) != 8: # valide antes: poupa uma chamada
return {"cep": str(texto), "situacao": "invalido"}
status, j = baixar(CEP_API + cep)
if status == 404:
return {"cep": cep, "situacao": "nao_encontrado"}
if status == 400:
return {"cep": cep, "situacao": "invalido"}
if status != 200:
return {"cep": cep, "situacao": f"erro_{status}"}
return {"cep": cep, "situacao": "encontrado", "logradouro": j["address"], "bairro": j["district"],
"cidade": j["city"], "uf": j["state"], "ibge": j["city_ibge"], "ddd": j["ddd"],
"lat": numero(j["lat"]), "lng": numero(j["lng"]), "cep_geral": j["address"] == ""}
Por que assim:
baixardevolve o status em vez de levantar exceção. OurlliblevantaHTTPErrorem qualquer 4xx e 5xx. Aqui, 400 e 404 são respostas normais, que viram uma "situação" na sua tabela.- A retentativa só vale para 502, 503 e 504, os erros de servidor que costumam passar na segunda tentativa. Um 404 ou um 400 não muda se você repetir.
- A validação dos 8 dígitos vem antes da chamada. Um CEP com 7 dígitos nunca chega à API: você economiza a chamada e já sabe a resposta.
- O
cep_geralmarca o CEP de cidade (rua vazia), para você decidir o que fazer com ele.
4 Junte com o IBGE pelo código do município
Com o city_ibge na mão, a API de localidades do IBGE devolve nome, UF e região:
curl -s "https://servicodados.ibge.gov.br/api/v1/localidades/municipios/3550308"
A resposta traz o município com a microrregião, a região imediata e, aninhada dentro delas, a UF e a região ("regiao":{"id":3,"sigla":"SE","nome":"Sudeste"}). A função abaixo pega UF e região pela região imediata, porque o IBGE está substituindo meso e microrregiões pelas regiões imediatas e intermediárias:
cache_mun = {}
def municipio_ibge(codigo):
if codigo not in cache_mun:
status, j = baixar(IBGE_MUN + codigo)
m = (j[0] if j else None) if isinstance(j, list) else j # válido = objeto; inexistente = lista vazia
uf = m and m["regiao-imediata"]["regiao-intermediaria"]["UF"]
cache_mun[codigo] = m and {"nome": m["nome"], "uf": uf["sigla"], "regiao": uf["regiao"]["nome"]}
return cache_mun[codigo]
Duas pegadinhas do IBGE estão nessas linhas:
- O formato da resposta muda. Um código válido volta como objeto. Um código que não existe (
9999999), ou com 6 dígitos (355030), volta200com a lista vazia[], sem erro. E se você pedir vários códigos de uma vez, separados por|(3550308|3304557), volta uma lista. A primeira versão deste código tratava tudo como lista e quebrou comKeyError: 0. É por isso que há oisinstance. - O
cache_munevita repetir a chamada: com 100 CEPs de São Paulo, são 100 consultas de CEP e uma do município. O IBGE ainda mandacache-control: max-age=2592000(30 dias).
o IBGE responde gzip, mesmo quando o cliente pede Accept-Encoding: identity, e o urllib manda exatamente esse cabeçalho por padrão. O navegador descomprime sozinho, mas o urllib não. Sem o gzip.decompress do baixar, a resposta vira bytes ilegíveis e dá o erro que está em Erros comuns.
5 Rode para uma lista de CEPs
A lista de teste é a do próprio dashboard, com um CEP que não existe (99999999), um com hífen (01001-000) e um inválido (1001000). Seis chamadas ao mesmo tempo, como o dashboard faz:
ceps = ["01001000", "01310100", "20040002", "30130010", "70150900", "74003010", "40026280", "50030230",
"60165121", "69005070", "66013010", "69301000", "80020310", "90010150", "88010000", "37925000",
"99999999", "01001-000", "1001000"]
with ThreadPoolExecutor(6) as ex:
res = list(ex.map(consultar_cep, ceps))
print(dict(Counter(r["situacao"] for r in res)))
achados = [r for r in res if r["situacao"] == "encontrado"]
print("CEPs de cidade (sem logradouro):", [r["cep"] for r in achados if r["cep_geral"]])
regioes = Counter(municipio_ibge(r["ibge"])["regiao"] for r in achados)
print(dict(regioes))
print(municipio_ibge("3550308"))
print(municipio_ibge("9999999"))
{'encontrado': 17, 'nao_encontrado': 1, 'invalido': 1}
CEPs de cidade (sem logradouro): ['37925000']
{'Sudeste': 6, 'Centro-Oeste': 2, 'Nordeste': 3, 'Norte': 3, 'Sul': 3}
{'nome': 'São Paulo', 'uf': 'SP', 'regiao': 'Sudeste'}
None
Lendo a saída: das 19 entradas, 17 foram encontradas, 1 não existe e 1 é inválida. O 01001-000 e o 01001000 são o mesmo endereço, a Praça da Sé, por isso aparecem dois achados dele. O 37925000 é o CEP de cidade (Piumhi/MG). E a junção com o IBGE agrupou os 17 achados por região.
Nesse teste, 60 CEPs diferentes com seis chamadas ao mesmo tempo, em menos de um segundo, não trouxeram nenhum 429. Isso é o que observei: a documentação do CEP não declara limite. Por prudência, o dashboard usa no máximo seis chamadas simultâneas e 100 CEPs por consulta.
6 O mapa: a malha do IBGE
Para desenhar o município, a API de malhas devolve o contorno em GeoJSON:
curl -s "https://servicodados.ibge.gov.br/api/v3/malhas/municipios/3550308?formato=application/vnd.geo+json&qualidade=intermediaria"
A resposta é uma FeatureCollection com o polígono do município e a propriedade "codarea": "3550308". O dashboard usa qualidade=minima para o contorno dos estados e qualidade=intermediaria para o dos municípios, e o formato também aceita SVG e TopoJSON.
um código de município que não existe (9999999) devolve HTTP 500, não 404. Não é falha do servidor: o erro é permanente, e repetir não adianta. Por isso a retentativa do passo 3 só vale para 502, 503 e 504. Use sempre o código que veio no city_ibge.
7 Power Query: o CEP no Power BI
Conforme a documentação oficial do Web.Contents (link nas referências); não executado aqui. Teste no seu Power BI Desktop antes de publicar.
let
ConsultaCep = (cep as text) as record =>
let
Limpo = Text.Select(cep, {"0".."9"}),
Resp = if Text.Length(Limpo) <> 8 then null else Web.Contents("https://cep.awesomeapi.com.br", [
RelativePath = "json/" & Limpo,
ManualStatusHandling = {400, 404}
]),
Status = if Resp = null then 400 else Value.Metadata(Resp)[Response.Status],
Json = if Status = 200 then Json.Document(Resp) else null,
Situacao = if Status = 200 then "encontrado" else if Status = 404 then "nao_encontrado" else "invalido"
in
[
CEP = Limpo,
Situacao = Situacao,
Logradouro = if Json = null then null else Json[address],
Cidade = if Json = null then null else Json[city],
UF = if Json = null then null else Json[state],
IBGE = if Json = null then null else Json[city_ibge],
DDD = if Json = null then null else Json[ddd],
Lat = if Json = null then null else try Number.FromText(Json[lat], "en-US") otherwise null,
Lng = if Json = null then null else try Number.FromText(Json[lng], "en-US") otherwise null
],
Fonte = #table({"CEP"}, {{"01001000"}, {"37925000"}, {"99999999"}, {"1001000"}}),
Consultado = Table.AddColumn(Fonte, "R", each ConsultaCep([CEP])),
Colunas = Table.ExpandRecordColumn(Consultado, "R",
{"Situacao", "Logradouro", "Cidade", "UF", "IBGE", "DDD", "Lat", "Lng"})
in
Colunas
Por que montar assim:
RelativePathcom o CEP no fim. A base fica fixa no primeiro argumento doWeb.Contents, e o Power BI Service consegue atualizar. Só o caminho muda a cada CEP.ManualStatusHandling = {400, 404}. Por padrão, um 404 vira erro na consulta inteira. Com a lista, os dois status viram uma linha comSituacao, como no Python.Number.FromText(..., "en-US"). A latitude vem como"-23.5500806", com ponto. Num Power BI em português, sem a cultura, o ponto vira separador de milhar.- Todos os registros têm os mesmos campos, com
null. Sem isso, oTable.ExpandRecordColumnfalha na primeira linha que não tiver o campo. - Troque o
#tabledaFontepela sua coluna de CEPs (por exemplo, uma tabela do Excel).
O que eu não consegui testar sem o Power BI: se o Web.Contents aceita o RelativePath montado com variável e como ele lê o status com ManualStatusHandling. E, para a junção com o IBGE no Power Query, repare que o IBGE devolve objeto, lista ou [] conforme o código (passo 4): trate os três antes de expandir.
ViaCEP e BrasilAPI: onde cada uma muda
Quem consulta CEP costuma conhecer também o ViaCEP e a BrasilAPI. Testei as três com o mesmo CEP em 07/11/2026. A diferença está no que cada uma chama de "não encontrado":
| AwesomeAPI | ViaCEP | BrasilAPI (v2) | |
|---|---|---|---|
CEP 01001000 | 200 | 200 | 200 |
| CEP não encontrado | 404 com not_found | 200 com {"erro":"true"} | 404 com CepPromiseError |
| CEP de formato inválido | 400 com invalid | 400 com página HTML | 400 com validation_error |
| Latitude e longitude | Sim | Não | Sim |
| CORS | * | * | não testado |
o CEP 99999999 voltou 404 na AwesomeAPI, {"erro":"true"} no ViaCEP e 200 na BrasilAPI, com a Avenida das Torres, em Sarandi/PR. Cada serviço usa uma base. "Não encontrado" numa API não prova que o CEP não existe: é o resultado daquela base.
A escolha depende do campo de que você precisa. O dashboard usa a AwesomeAPI porque ela traz coordenada, DDD e código IBGE na mesma chamada. Se você não precisa de mapa, o ViaCEP também devolve o código IBGE e ainda traz regiao, mas sem coordenadas.
Erros comuns
CEP com menos ou mais de 8 dígitos, ou com letras (HTTP 400):
{"code":"invalid","message":"CEP invalido, tente: 00000000"}
Causa: 1001000, 010010000 ou abc12345. Correção: tire tudo que não for dígito e confira se sobraram 8 antes de chamar, como no passo 3.
CEP que a base não tem (HTTP 404):
{"code":"not_found","message":"O CEP 99999999 nao foi encontrado"}
Causa: formato válido, mas a base da AwesomeAPI não tem o CEP. Correção: trate como "não encontrado" e, se importar, confira em outra base (veja a tabela acima).
Resposta do IBGE ilegível no Python:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x8b in position 1: invalid start byte
Causa: o IBGE respondeu gzip e o urllib não descomprime. O 0x8b é o segundo byte do cabeçalho gzip. Correção: gzip.decompress quando o cabeçalho Content-Encoding for gzip, como no baixar.
KeyError: 0 ao ler o município do IBGE: o código válido volta como objeto, e não como lista. Correção: o isinstance do passo 4.
Malha de município inexistente (HTTP 500):
{"statusCode":500,"message":"Internal server error"}
Causa: código de município que não existe (9999999). Correção: use o city_ibge que veio da consulta do CEP. Não repita a chamada.
Município inexistente nas localidades: 9999999 e 355030 (6 dígitos) voltam 200 com [], sem erro. Correção: trate a lista vazia, como o None do passo 4.
a AwesomeAPI manda cache-control: max-age=604800, public em todas as respostas, inclusive no 404. O que eu vi foi o cabeçalho; o efeito, que um navegador guarde por até 7 dias o "não encontrado" de um CEP novo, eu não testei. Se um CEP recém-criado seguir "não encontrado" no navegador, esse cache é a primeira suspeita.
Dica bônus: ache o CEP pelo endereço
A AwesomeAPI tem também uma busca por texto, que devolve até 10 resultados por chamada:
curl -s "https://cep.awesomeapi.com.br/search?q=paulista&state=SP&limit=2"
Executada em 07/11/2026, ela devolveu {"results":[{...},{...}]}, e cada item tem os mesmos campos da consulta por CEP, mais state_name. Na documentação, há filtros por coordenada e distância (lat, lng e d, em km), e a paginação (page) vale só para quem tem chave. Não é o que o dashboard usa, mas resolve o caminho contrário: do nome da rua ao CEP.
Conclusão
Agora você consulta CEP separando o que é "não existe" (404), "formato errado" (400) e "CEP de cidade" (rua vazia), junta o resultado com o IBGE pelo código do município e sabe por que a resposta do IBGE precisa do gzip.decompress no Python e de três tratamentos de formato.
Se você quer ligar o Power BI a APIs como essas e montar o painel com dados que se atualizam sozinhos, é isso que o curso Chora API ensina, com Linguagem M e DAX no contexto real. Qualquer dúvida, comenta aqui, na comunidade #conectados.
Quer ligar o Power BI a APIs como essas?
Na Formação Chora API você aprende a ligar o Power BI a uma API e montar o painel com dados atualizados, com Linguagem M e DAX no contexto do curso.
Ver a Formação →Referências
Documentação oficial (consultada em 07/11/2026):
- API CEP — AwesomeAPI
- API Busca de Endereços — AwesomeAPI
- API de localidades — IBGE
- API de malhas geográficas, versão 3 — IBGE
- Web.Contents — Microsoft Learn
- Refresh and dynamic data sources — Microsoft Learn
Outros serviços de CEP (para comparação, consultados em 07/11/2026):
- ViaCEP — webservice gratuito de CEP
- BrasilAPI, CEP v2