Moradas API beta

API gratuita para autocompletar moradas e códigos postais (CP7) de Portugal. Demo ao vivo →

Especificação OpenAPI 3.1: openapi.json

Começar em 2 minutos

Sem registo nem chave de API: copie um exemplo e execute-o. Três pedidos cobrem um checkout — sugestões enquanto o cliente escreve, o CP7 exato pelo número de porta e a ficha de um código postal. Os comentários mostram a resposta real.

# 1. Suggestions as the user types (a trailing door number picks the exact CP7)
curl "https://moradas.dev/suggest?q=av%20liberdade%20lisboa%20196&count=3"

# 2. Exact CP7 for a street and door number
#    art_id comes from /suggest (read here with jq) and changes when the data is refreshed: never store it
ART=$(curl -s "https://moradas.dev/suggest?q=av%20liberdade%20lisboa&count=1" | jq '.suggestions[0].data.art_id')
curl "https://moradas.dev/resolve?art=$ART&numero=196"

# 3. Postal code card: locality, municipality, district and streets
curl "https://moradas.dev/cp/1000-098"

# Errors: -i prints the status line and headers (404 = not found, and the X-Moradas-Hint header says why;
#         429 = too many requests)
curl -i "https://moradas.dev/cp/9999-999"

Erros, limites e formato do CP7

GET /suggest

Um único endpoint para tudo o que o checkout precisa. Dígitos são tratados como código postal, texto como rua ou localidade. Um número no fim é entendido como número de porta e escolhe o CP7 exato quando possível.

GET /suggest?q=estrada%20malveira%20da%20serra%20920&count=2

{ "suggestions": [
  { "value": "Estrada Malveira da Serra, Malveira da Serra",
    "data": { "art_id": 125494, "kind": "street", "street": "Estrada Malveira da Serra",
              "localidade": "Malveira da Serra", "concelho": "Cascais", "distrito": "Lisboa",
              "cp7": "2755-332", "nseg": 1, "numero": "920", "resolved_cp7": "2755-332" } },
  { "value": "Estrada Malveira da Serra, Aldeia de Juzo",
    "data": { "art_id": 126122, "kind": "street", "street": "Estrada Malveira da Serra",
              "localidade": "Aldeia de Juzo", "concelho": "Cascais", "distrito": "Lisboa",
              "cp7": "2750-834", "nseg": 11, "numero": "920", "resolved_cp7": "2750-834" } }
] }

Os exemplos desta página são respostas reais de 30.09.2026. Os valores de art_id mudam a cada atualização semanal dos dados: tire-os sempre de uma resposta nova do /suggest e nunca os copie destes exemplos para o seu código.

O mesmo nome de rua pode existir em várias localidades postais (aqui Malveira da Serra e Aldeia de Juzo). nseg é o número de troços de código postal nessa rua; com nseg > 1, resolved_cp7 significa que o número de porta correspondeu a um troço específico.

parâmetrosignificado
qcódigo postal (1000-098, 4–7 dígitos) ou texto livre (av liberdade lisboa 196)
countmáximo de sugestões, 10 por defeito, limite 20

A pesquisa é tolerante: acentos opcionais (sao joao), abreviaturas (av → Avenida), preposições opcionais (rua das flores também encontra «Rua Flores»). Cidades maiores aparecem primeiro.

GET /resolve

Quando uma rua tem vários códigos postais ("cp7": null, "nseg" > 1), resolva pelo número de porta — a paridade e os intervalos de números decidem. O art_id é efémero: muda quando os dados são atualizados — use-o logo a seguir ao suggest e não o guarde. Uma sugestão com "kind": "loc", "art_id": 0 e "cp7": null é a localidade ou o concelho inteiro (por exemplo, ao escrever só «Lisboa»): não há rua a resolver, peça à pessoa que continue a escrever a morada. O início do nome de um concelho ou de uma localidade grande («Lisb», «Carcav») devolve o próprio lugar primeiro. Se o resultado só apareceu depois de corrigir uma gralha («rua agusta» → Rua Augusta), a sugestão traz "corrected": true em data.

GET /resolve?art=132467&numero=10        (art_id of "Rua das Adelas, Lisboa" in a fresh /suggest)

{ "resolved": { "value": "Rua das Adelas, Lisboa",
                "data": { "art_id": 132467, "kind": "street", "street": "Rua das Adelas",
                          "localidade": "Lisboa", "concelho": "Lisboa", "distrito": "Lisboa",
                          "cp7": "1200-008", "nseg": 2 } },
  "segments": [ { "cp7": "1200-007", "label": "Impares de 3 a 17A" },
                { "cp7": "1200-008", "label": "Pares de 2 a 28" } ] }

Sem numero, ou com um número que não decide um único código, resolved vem null e segments continua a listar os troços, para o cliente escolher. Um art_id que já não existe devolve 404 ({"error":"unknown artery"}): volte a chamar o /suggest.

GET /cp/{cp7}

Ficha de um código postal — localidade, concelho, distrito e as ruas que abrange.

GET /cp/1000-098

{ "cp7": "1000-098", "cp4": "1000", "cp3": "098",
  "distrito": "Lisboa", "concelho": "Lisboa", "localidade": "Lisboa",
  "arterias": [ { "art_id": 130446, "street": "Praça do Chile",
                  "troco": null, "porta": null, "cliente": null } ] }

As ruas estão em arterias[].street. troco é o intervalo de números de porta que o código cobre (por exemplo Pares de 2 a 28), porta um número de porta concreto e cliente a organização que tem o código só para si; os três vêm null quando não se aplicam. Um código sem ruas (uma localidade inteira) tem arterias vazio.

Quando não há resposta, o 404 traz o cabeçalho X-Moradas-Hint com o motivo em inglês; o corpo não muda. Exemplos: /cp/4700-000 → 4700-000 is not an assigned postal code; /cp/4700 lists the postal codes that start with 4700; /cp/1100-413%20LISBOA → Use only the postal code, without locality or other text: /cp/1100-413. O cabeçalho é legível a partir do browser (Access-Control-Expose-Headers).

GET /cp/{cp4} — todos os códigos de uma zona

Com só os quatro primeiros dígitos, a resposta é a lista de todos os CP7 dessa zona, por ordem, cada um com a sua localidade, concelho e distrito (os mesmos que /cp/{cp7} devolve). cliente traz o nome da organização quando o código é só dela; senão, null. Uma zona sem códigos devolve 404. Zonas grandes têm perto de mil códigos: guarde a lista em cache.

GET /cp/4700        (shortened here to the first three codes)

{ "cp4": "4700",
  "codigos": [
    { "cp7": "4700-001", "localidade": "Braga", "concelho": "Braga", "distrito": "Braga", "cliente": null },
    { "cp7": "4700-002", "localidade": "Braga", "concelho": "Braga", "distrito": "Braga", "cliente": null },
    { "cp7": "4700-003", "localidade": "Braga", "concelho": "Braga", "distrito": "Braga", "cliente": null }
  ] }

Caminhos e campos que não existem

POST /batch

Para programas que têm uma lista de moradas — importação de encomendas, CRM, limpeza de uma base de clientes. Até 100 moradas num só pedido, cada uma passada pela mesma lógica do /suggest (um número de porta no texto escolhe o CP7 exato; um código postal é procurado como tal). A resposta vem pela ordem de entrada, uma linha por morada, com um veredicto: ok — código postal determinado, em cp7; ambiguous — sem resposta única (vários candidatos sem nada que os distinga, uma rua com vários códigos postais e sem número de porta, ou só uma localidade); not_found — nada encontrado. suggestion é a primeira sugestão tal como o /suggest a devolve (com resolved_cp7 quando o número de porta decidiu, corrected: true depois de corrigir uma gralha); em ambiguous é o melhor candidato, para alguém confirmar; em not_found é null.

POST /batch
Content-Type: application/json

{ "items": [
  { "id": "o-1001", "q": "Avenida da Liberdade 196, Lisboa" },
  { "id": "o-1002", "q": "Rua Augusta 100 Lisboa" },
  { "id": "o-1003", "q": "xyzxyz qqq" },
  { "id": "o-1004", "q": "rua das flores" }
] }
{ "results": [
  { "id": "o-1001", "q": "Avenida da Liberdade 196, Lisboa",
    "status": "ok", "cp7": "1250-147",
    "suggestion": { "value": "Avenida da Liberdade, Lisboa",
      "data": { "art_id": 129534, "kind": "street",
                "street": "Avenida da Liberdade", "localidade": "Lisboa",
                "concelho": "Lisboa", "distrito": "Lisboa", "cp7": "1250-147",
                "nseg": 28, "numero": "196", "resolved_cp7": "1250-147" } } },
  { "id": "o-1002", "q": "Rua Augusta 100 Lisboa",
    "status": "ok", "cp7": "1100-053",
    "suggestion": { "value": "Rua Augusta, Lisboa",
      "data": { "art_id": 130802, "kind": "street", "street": "Rua Augusta",
                "localidade": "Lisboa", "concelho": "Lisboa", "distrito": "Lisboa",
                "cp7": "1100-053", "nseg": 12, "numero": "100",
                "resolved_cp7": "1100-053" } } },
  { "id": "o-1003", "q": "xyzxyz qqq",
    "status": "not_found", "cp7": null,
    "suggestion": null },
  { "id": "o-1004", "q": "rua das flores",
    "status": "ambiguous", "cp7": null,
    "suggestion": { "value": "Rua das Flores, Lisboa",
      "data": { "art_id": 132486, "kind": "street", "street": "Rua das Flores",
                "localidade": "Lisboa", "concelho": "Lisboa", "distrito": "Lisboa",
                "cp7": null, "nseg": 5 } } }
] }

Resposta real de 02.10.2026 (os art_id mudam a cada atualização dos dados). A primeira linha tem rua, número e localidade: fica ok. A quarta não diz a localidade e «Rua das Flores» existe em várias — fica ambiguous, com a melhor candidata para alguém decidir. Uma morada só fica ok quando a primeira sugestão tem código postal e é líder clara: é a única; todas as candidatas dão o mesmo código; o número de porta só correspondeu a ela; ou cobre mais palavras do pedido do que qualquer outra («rua das flores dona maria» → Dona Maria, não Lisboa).

camposignificado
items[].qa morada em texto livre, como q no /suggest: rua, número de porta e localidade, ou um código postal. Obrigatório.
items[].idqualquer texto ou número seu (número da encomenda, id do registo), devolvido tal e qual. Opcional; na resposta vem null quando falta.
results[].statusok, ambiguous ou not_found.
results[].cp7o código postal quando ok; senão null.
results[].suggestiona primeira sugestão, no formato do /suggest; null em not_found.

Limites. Até 100 moradas e 64 KB por pedido. Um pedido com N moradas conta como N pedidos no limite geral (cerca de 50 por 10 segundos por IP): um pacote maior do que o que ainda cabe é recusado inteiro com 429 e Retry-After, sem processar nenhuma linha — na prática, envie até 50 moradas de cada vez e, num 429, espere os segundos do cabeçalho. Mais de 100 moradas → 400 com hint; corpo maior → 413; JSON inválido → 400; GET /batch → 405 com X-Moradas-Hint. As respostas não ficam em cache. CORS aberto, como no /suggest.

curl -s -X POST "https://moradas.dev/batch" -H "Content-Type: application/json" \
  -d '{"items":[{"id":"o-1001","q":"Avenida da Liberdade 196, Lisboa"},
             {"id":"o-1003","q":"xyzxyz qqq"}]}'
# {"results":[{"id":"o-1001","q":"Avenida da Liberdade 196, Lisboa","status":"ok",
#              "cp7":"1250-147","suggestion":{…}},
#             {"id":"o-1003","q":"xyzxyz qqq","status":"not_found",
#              "cp7":null,"suggestion":null}]}
# Python 3.8+ with requests (pip install requests)
import time

import requests


def moradas_batch(items):
    """items: [{"id": ..., "q": "..."}], up to 50 per call."""
    for attempt in range(3):
        res = requests.post("https://moradas.dev/batch",
                            json={"items": items}, timeout=60)
        if res.status_code == 429:              # the batch counts as len(items) requests
            wait = res.headers.get("Retry-After", "10")
            time.sleep(int(wait) if wait.isdigit() else 10)
            continue
        res.raise_for_status()                  # 400 / 413: the body has "error" and "hint"
        return res.json()["results"]
    raise RuntimeError("Moradas: still rate limited after 3 attempts")


rows = [{"id": "o-1001", "q": "Avenida da Liberdade 196, Lisboa"},
        {"id": "o-1003", "q": "xyzxyz qqq"}]
for start in range(0, len(rows), 50):           # 50 at a time stays inside the rate limit
    for r in moradas_batch(rows[start:start + 50]):
        print(r["id"], r["status"], r["cp7"])   # o-1001 ok 1250-147  /  o-1003 not_found None

POST /feedback

Uma morada não apareceu? Envie-a aqui.

POST /feedback
Content-Type: application/json

{ "query": "rua que não apareceu, localidade", "note": "optional details" }

Widget (qualquer formulário)

Duas linhas: carregar o script e ligá-lo ao campo da morada. As sugestões aparecem enquanto se escreve; os campos indicados em fill recebem código postal, localidade, concelho e distrito. Sem build, sem dependências.

<script src="https://moradas.dev/widget.js"></script>
<script>
  PTAddress.attach(document.querySelector('#morada'), {
    fill: { postal: '#cp7', city: '#localidade', municipality: '#concelho', district: '#distrito' },
    lang: 'pt'
  });
</script>

Carregue o script sem async e chame attach depois de o campo existir na página (por exemplo, no fim do <body>). Um CP7 que o cliente já escreveu não é apagado quando a rua escolhida tem vários códigos postais: o widget preenche o CP7 exato quando o cliente escreve o número de porta. Se o serviço não responder (limite de pedidos ou falha), a lista avisa que está temporariamente indisponível e o cliente continua a escrever normalmente.

Opções do widget

opçãosignificado
fillCampos a preencher: { postal, city, municipality, district }, cada um um seletor CSS ou um elemento, para o CP7, a localidade, o concelho e o distrito. Todos opcionais.
onSelect(data)Chamado quando o cliente escolhe uma sugestão; data é o objeto data dessa sugestão no /suggest (street, localidade, concelho, distrito, cp7…).
onResolve(cp7)Chamado quando o CP7 exato fica conhecido: na escolha ou, numa rua com vários códigos postais, quando o cliente escreve o número de porta e sai do campo.
lang'pt' (por defeito) ou 'en': língua das mensagens da lista.
minCharsCaracteres escritos antes do primeiro pedido; 2 por defeito.
debouncePausa depois da última tecla, em milissegundos; 160 por defeito.
countMáximo de sugestões na lista; 8 por defeito, limite 20.
endpointURL base da API; https://moradas.dev por defeito.
beacontrue por defeito: envia um relatório de erro mínimo quando o serviço falha (ver abaixo). false desliga-o.

Relatório de erros: quando o serviço não responde como devia (limite de pedidos 429, falha 5xx, erro de rede ou resposta inesperada), o widget envia com navigator.sendBeacon um pequeno POST para {endpoint}/widget-error, por exemplo {"kind": "429", "widget_version": "0.3.0", "http_status": 429} — no máximo um por tipo de erro e por carregamento da página. Não leva morada, texto escrito nem o endereço da página; o browser junta a origem do site, como em qualquer pedido. Contamos estes relatórios por hora e por site para ver avarias que de outra forma não veríamos. O pedido aparece no separador Network do DevTools; desligue-o com beacon: false. Mais em Privacidade.

PTAddress.attach(input, options) devolve { destroy() }, que remove a lista e os listeners do campo.

React, Vue e outras SPA

Ligue o widget quando o campo já existe no DOM e desligue-o quando o componente sai: attach no useEffect ou no onMounted, destroy() na limpeza. Os campos de fill são preenchidos como se o cliente os escrevesse (eventos input e change), por isso onChange e v-model recebem os valores. Para escrever os valores no estado diretamente, use onSelect e onResolve, como no exemplo React.

// React 18+. Load the widget once, e.g. in index.html:
//   <script src="https://moradas.dev/widget.js"></script>
import { useEffect, useRef, useState } from 'react';

const EMPTY = { morada: '', cp7: '', localidade: '', concelho: '' };

export function AddressForm() {
  const [form, setForm] = useState(EMPTY);
  const moradaRef = useRef(null);

  useEffect(() => {
    const widget = window.PTAddress.attach(moradaRef.current, {
      onSelect: (d) => setForm((f) => ({
        ...f, localidade: d.localidade, concelho: d.concelho,
      })),
      onResolve: (cp7) => setForm((f) => ({ ...f, cp7 })),
    });
    return () => widget.destroy(); // cleanup on unmount
  }, []);

  const field = (name) => ({
    name,
    value: form[name],
    onChange: (e) => setForm((f) => ({ ...f, [name]: e.target.value })),
  });

  return (
    <form>
      <input ref={moradaRef} {...field('morada')} placeholder="Morada" />
      <input {...field('cp7')} placeholder="Código postal" />
      <input {...field('localidade')} placeholder="Localidade" />
    </form>
  );
}
<!-- Vue 3. Load https://moradas.dev/widget.js once, e.g. in index.html -->
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';

const form = ref({ morada: '', cp7: '', localidade: '' });
const moradaInput = ref(null);
let widget = null;

onMounted(() => {
  widget = window.PTAddress.attach(moradaInput.value, {
    // filled in like typing, so v-model picks up the values
    fill: { postal: '#cp7', city: '#localidade' },
  });
});
onBeforeUnmount(() => widget?.destroy());
</script>

<template>
  <input ref="moradaInput" v-model="form.morada" placeholder="Morada">
  <input id="cp7" v-model="form.cp7" placeholder="Código postal">
  <input id="localidade" v-model="form.localidade" placeholder="Localidade">
</template>

Plugins prontos: WooCommerce e PrestaShop

WooCommerce — plugin 0.2.1, para instalar a partir do ficheiro: ZIP (SHA-256) › Plugins › Adicionar novo › Carregar plugin › Instalar agora › Ativar. Checkout em blocos e clássico; WordPress 6.5+, PHP 7.4+, WooCommerce 9.9+ para o checkout em blocos. Em revisão no wordpress.org — até lá, as atualizações são manuais. Passos e notas no guia WooCommerce.

PrestaShop — módulo 0.2.1 para 1.7.1 a 9: ZIP (SHA-256) › Módulos › Gestor de módulos › Carregar um módulo. Carrega o widget.js e liga-o aos campos address1, postcode e city no checkout e na área de cliente, só com Portugal como país; não testado numa loja real. Código e notas no guia PrestaShop.

MCP (agentes de IA)

Servidor MCP remoto — nada para instalar, sem chave. Qualquer agente compatível com MCP (Claude, Cursor, ChatGPT e outros) pode autocompletar moradas portuguesas, consultar códigos postais e verificar listas inteiras de moradas através dele.

Endpoint:   https://moradas.dev/mcp   (Streamable HTTP, stateless)
Tools:      suggest_address · resolve_postal_code · postal_code_info · verify_addresses

Claude Code:
  claude mcp add --transport http moradas https://moradas.dev/mcp

Cursor / Windsurf / others (mcp.json):
  { "mcpServers": { "moradas": { "url": "https://moradas.dev/mcp" } } }

Ferramentas

Exemplo de tools/call para verify_addresses e resposta real de 06.10.2026 (os art_id mudam a cada atualização dos dados). structuredContent é o JSON; content[0].text traz o mesmo JSON em texto, para clientes sem saída estruturada.

POST /mcp
Content-Type: application/json

{ "jsonrpc": "2.0", "id": 1, "method": "tools/call",
  "params": { "name": "verify_addresses", "arguments": { "items": [
    { "id": "o-1001", "q": "Avenida da Liberdade 196, Lisboa" },
    { "id": 2, "q": "rua das flores" },
    { "q": "xyzxyz qqq" }
  ] } } }
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"results\":[{\"id\":\"o-1001\",\"q\":\"Avenida da Liberdade 196, Lisboa\",\"status\":\"ok\",\"cp7\":\"1250-147\",\"suggestion\":{\"value\":\"Avenida da Liberdade, Lisboa\",\"data\":{\"art_id\":129377,\"kind\":\"street\",\"street\":\"Avenida da Liberdade\",\"localidade\":\"Lisboa\",\"concelho\":\"Lisboa\",\"distrito\":\"Lisboa\",\"cp7\":\"1250-147\",\"nseg\":28,\"numero\":\"196\",\"resolved_cp7\":\"1250-147\"}}},{\"id\":2,\"q\":\"rua das flores\",\"status\":\"ambiguous\",\"cp7\":null,\"suggestion\":{\"value\":\"Rua das Flores, Lisboa\",\"data\":{\"art_id\":132329,\"kind\":\"street\",\"street\":\"Rua das Flores\",\"localidade\":\"Lisboa\",\"concelho\":\"Lisboa\",\"distrito\":\"Lisboa\",\"cp7\":null,\"nseg\":5}}},{\"id\":null,\"q\":\"xyzxyz qqq\",\"status\":\"not_found\",\"cp7\":null,\"suggestion\":null}]}"
      }
    ],
    "structuredContent": {
      "results": [
        {
          "id": "o-1001",
          "q": "Avenida da Liberdade 196, Lisboa",
          "status": "ok",
          "cp7": "1250-147",
          "suggestion": {
            "value": "Avenida da Liberdade, Lisboa",
            "data": {
              "art_id": 129377,
              "kind": "street",
              "street": "Avenida da Liberdade",
              "localidade": "Lisboa",
              "concelho": "Lisboa",
              "distrito": "Lisboa",
              "cp7": "1250-147",
              "nseg": 28,
              "numero": "196",
              "resolved_cp7": "1250-147"
            }
          }
        },
        {
          "id": 2,
          "q": "rua das flores",
          "status": "ambiguous",
          "cp7": null,
          "suggestion": {
            "value": "Rua das Flores, Lisboa",
            "data": {
              "art_id": 132329,
              "kind": "street",
              "street": "Rua das Flores",
              "localidade": "Lisboa",
              "concelho": "Lisboa",
              "distrito": "Lisboa",
              "cp7": null,
              "nseg": 5
            }
          }
        },
        {
          "id": null,
          "q": "xyzxyz qqq",
          "status": "not_found",
          "cp7": null,
          "suggestion": null
        }
      ]
    },
    "isError": false
  }
}

Utilização

Beta. URL base: https://moradas.dev. Feedback: dev@moradas.dev