Validar um CEP tem duas etapas diferentes: conferir o formato (oito dígitos, com hífen opcional depois do quinto) e conferir se o código existe. A expressão regular resolve a primeira. Só uma consulta a uma base de CEPs resolve a segunda. Este guia traz a regex, a máscara do campo, a checagem pela faixa do estado e exemplos em JavaScript, PHP e Python.
Regex do formato
A forma mais citada é ^\d{5}-?\d{3}$: começo da string, cinco dígitos, hífen opcional, três dígitos e fim da string. Sem as âncoras ^ e $, a regex encontraria um “CEP” no meio de um CPF ou de um telefone.
Prefira escrever [0-9] no lugar de \d. Em JavaScript e no PHP sem o modificador u, as duas formas são equivalentes, mas no Python 3 o \d aceita também dígitos de outros sistemas de escrita, como os usados no árabe e no devanágari. Com [0-9], a mesma regex funciona igual nas três linguagens.
Não valide CEP como número. Um campo numérico apaga o zero à esquerda, e todo CEP de 01000-000 a 09999-999, do estado de São Paulo, começa com zero. Guarde e compare o CEP como texto.
Normalizar a entrada sem perder o zero
Pessoas digitam CEP com espaço, ponto ou sem hífen. Para aceitar essas variações, remova tudo o que não for dígito e confira se sobraram exatamente oito:
function normalizarCep(entrada) {
const digitos = entrada.replace(/[^0-9]/g, '')
return digitos.length === 8 ? digitos : null
}
normalizarCep('01.310-100') // '01310100'
normalizarCep('1310100') // null: faltou um dígitoComo o valor continua sendo texto, o zero inicial fica. Não corte sequências maiores para caber em oito dígitos: se a pessoa colou um CPF ou um telefone, o resultado seria um CEP com formato válido e endereço errado. Para exibir, junte as partes com o hífen: os cinco primeiros dígitos, o hífen e os três últimos.
Máscara no campo com JavaScript puro
<label for="cep">CEP</label>
<input id="cep" name="cep" type="text" inputmode="numeric"
autocomplete="postal-code" placeholder="00000-000"
pattern="[0-9]{5}-?[0-9]{3}">- type="text" mantém o zero à esquerda; um campo numérico não mantém.
- inputmode="numeric" abre o teclado numérico no celular sem transformar o valor em número.
- autocomplete="postal-code" permite que o navegador preencha o CEP salvo da pessoa.
- pattern faz a validação nativa do navegador; o atributo já exige que o valor inteiro corresponda, então não leva ^ nem $.
- Sem maxlength: o navegador aplica esse limite antes do script e cortaria um CEP colado com ponto ou espaço, como 01.310-100, que viraria 01310-10. A máscara e o pattern já cuidam do tamanho.
const campo = document.querySelector('#cep')
campo.addEventListener('input', () => {
const digitos = campo.value.replace(/[^0-9]/g, '')
if (digitos.length > 8) return // deixe a validação avisar; não corte o que foi colado
if (digitos.length > 5) {
campo.value = digitos.slice(0, 5) + '-' + digitos.slice(5)
} else {
campo.value = digitos
}
})A máscara só organiza o que foi digitado. Não bloqueie colar no campo e mostre a mensagem de erro perto dele. Para limpar uma lista que já existe, o formatador de CEP faz o trabalho no navegador.
Formato válido não é CEP existente
99999-999 passa em qualquer regex de CEP; para saber se ele existe, é preciso consultar. Em outubro de 2026, por exemplo, a ViaCEP respondia a esse código com o campo erro. As APIs públicas mais usadas documentam as respostas:
- ViaCEP: responde com HTTP 400 quando o formato é inválido e com o campo
erroquando o CEP de oito dígitos não é encontrado. - BrasilAPI: oferece um endpoint de CEP; confira na documentação como ele responde a um código inexistente.
Trate três resultados de forma separada: formato inválido (peça para corrigir), CEP não encontrado (peça para conferir e permita preencher o endereço à mão) e serviço indisponível (não bloqueie o cadastro por causa disso). O guia como usar uma API de CEP detalha a integração, inclusive o cuidado com o volume de consultas.
Checagem rápida pela faixa do estado
Se o formulário também pede a UF, compare os primeiros dígitos do CEP com a faixa do estado. É uma checagem barata, feita sem rede, que pega erros como um CEP do Rio de Janeiro num endereço de São Paulo. A tabela completa está na faixa de CEP por estado.
// Faixas gerais por estado publicadas pelos Correios (há exceções)
const FAIXAS = {
SP: [['01000000', '19999999']],
RJ: [['20000000', '28999999']],
DF: [['70000000', '72799999'], ['73000000', '73699999']],
// ...demais estados
}
function cepCombinaComUf(cep, uf) {
const digitos = cep.replace(/[^0-9]/g, '')
return digitos.length === 8 &&
(FAIXAS[uf] || []).some(([inicio, fim]) => digitos >= inicio && digitos <= fim)
}Com oito dígitos dos dois lados, a comparação de texto funciona; por isso a função recusa qualquer outro tamanho, como um CEP que perdeu o zero inicial. Use o resultado como aviso, não como bloqueio: as faixas são gerais, e há exceções, como grandes usuários e caixas postais.
Exemplos em JavaScript, PHP e Python
Os três exemplos validam o formato e depois consultam a ViaCEP, devolvendo um status que a interface pode usar para decidir a mensagem.
JavaScript
const CEP_REGEX = /^[0-9]{5}-?[0-9]{3}$/
async function validarCep(entrada) {
const texto = entrada.trim()
if (!CEP_REGEX.test(texto)) return { status: 'formato-invalido' }
const cep = texto.replace('-', '')
try {
const resposta = await fetch('https://viacep.com.br/ws/' + cep + '/json/')
if (!resposta.ok) return { status: 'indisponivel' }
const dados = await resposta.json()
if (dados.erro) return { status: 'nao-encontrado' }
return { status: 'ok', cep: cep.slice(0, 5) + '-' + cep.slice(5), dados }
} catch (erro) {
return { status: 'indisponivel' }
}
}PHP
<?php
function validarCep(string $entrada): array
{
$texto = trim($entrada);
if (!preg_match('/^[0-9]{5}-?[0-9]{3}$/', $texto)) {
return ['status' => 'formato-invalido'];
}
$cep = str_replace('-', '', $texto);
$contexto = stream_context_create(['http' => ['timeout' => 5]]);
$corpo = @file_get_contents('https://viacep.com.br/ws/' . $cep . '/json/', false, $contexto);
$dados = $corpo === false ? null : json_decode($corpo, true);
if (!is_array($dados)) {
return ['status' => 'indisponivel'];
}
if (isset($dados['erro'])) {
return ['status' => 'nao-encontrado'];
}
return ['status' => 'ok', 'dados' => $dados];
}Python
import json
import re
import urllib.error
import urllib.request
CEP_REGEX = re.compile(r'[0-9]{5}-?[0-9]{3}')
def validar_cep(entrada: str) -> dict:
texto = entrada.strip()
if not CEP_REGEX.fullmatch(texto):
return {'status': 'formato-invalido'}
cep = texto.replace('-', '')
url = f'https://viacep.com.br/ws/{cep}/json/'
try:
with urllib.request.urlopen(url, timeout=5) as resposta:
dados = json.load(resposta)
except (urllib.error.URLError, TimeoutError, ValueError):
return {'status': 'indisponivel'}
if 'erro' in dados:
return {'status': 'nao-encontrado'}
return {'status': 'ok', 'dados': dados}No Python, fullmatch exige que a string inteira corresponda, então a regex não precisa de ^ e $. No PHP, preg_match devolve 1 quando há correspondência e 0 quando não há.
CEPs para testar
Casos de teste para o campo de CEP
| Entrada | Resultado esperado |
|---|---|
| 01001-000 | Resultado esperado: Formato válido; CEP existente (Praça da Sé, São Paulo-SP) |
| 01001000 | Resultado esperado: Formato válido sem hífen |
| 1001000 | Resultado esperado: Inválido: sete dígitos, o zero foi perdido |
| 01.001-000 | Resultado esperado: Recusado pela regex; aceito se você normalizar antes |
| 99999-999 | Resultado esperado: Formato válido; a existência depende da consulta |
| 0100A-000 | Resultado esperado: Inválido: contém letra |
Para testes com endereços reais de vários estados, o gerador de CEP sorteia CEPs que existem no catálogo do RuaCEP. Use esses códigos só em testes, nunca como o endereço de alguém. Se os CEPs vêm de uma planilha, veja antes como formatar CEP no Excel para não perder o zero.
Perguntas frequentes
Qual é a regex de CEP?
^[0-9]{5}-?[0-9]{3}$, que aceita o CEP com ou sem hífen. Se você já removeu o hífen, use ^[0-9]{8}$.
A regex garante que o CEP existe?
Não. Ela confere só a escrita. Para saber se o código existe, consulte uma base de CEPs, como uma API ou a busca dos Correios.
Devo guardar o CEP com ou sem hífen no banco de dados?
Uma prática comum é guardar os oito dígitos como texto e mostrar o hífen na tela. Nunca use um tipo numérico, que perde o zero à esquerda.
Qual inputmode e autocomplete usar no campo de CEP?
inputmode="numeric" para o teclado numérico e autocomplete="postal-code" para o preenchimento automático do navegador.
Fontes
- MDN: Expressões regulares em JavaScript.
- MDN: atributo inputmode (em inglês).
- MDN: atributo autocomplete (em inglês).
- Manual do PHP: preg_match.
- Documentação do Python: módulo re.
- ViaCEP e BrasilAPI (documentação das APIs).

