API gratuita para autocompletar moradas e códigos postais (CP7) de Portugal. Demo ao vivo →
Especificação OpenAPI 3.1: openapi.json
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"
// Browser or Node.js 18+ (save as .mjs to use top-level await)
const BASE = 'https://moradas.dev';
async function moradas(path) {
for (let attempt = 0; attempt < 3; attempt++) {
const res = await fetch(BASE + path);
if (res.status === 429) { // too many requests: wait, then retry
const wait = Number(res.headers.get('Retry-After')) || 10;
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
continue;
}
if (res.status === 404) return null; // unknown postal code or art_id
if (!res.ok) throw new Error(`Moradas: HTTP ${res.status}`);
return res.json();
}
throw new Error('Moradas: still rate limited after 3 attempts');
}
// 1. Suggestions as the user types (a trailing door number picks the exact CP7)
const { suggestions } = await moradas('/suggest?q=' + encodeURIComponent('av liberdade lisboa 196') + '&count=3');
const first = suggestions[0];
console.log(first.value, first.data.cp7); // Avenida da Liberdade, Lisboa 1250-147
// 2. Exact CP7 for a street and door number (art_id changes when the data is refreshed: never store it)
const r = await moradas(`/resolve?art=${first.data.art_id}&numero=196`);
console.log(r?.resolved?.data.cp7 ?? 'no exact CP7'); // 1250-147
// 3. Postal code card (check the format first)
const cp7 = '1000-098';
if (/^\d{4}-\d{3}$/.test(cp7)) {
const card = await moradas('/cp/' + cp7);
console.log(card ? `${card.localidade}, ${card.concelho}, ${card.distrito}` : 'CP7 not found'); // Lisboa, Lisboa, Lisboa
}
<?php
// PHP 7.4+ with the curl extension
const BASE = 'https://moradas.dev';
function moradas(string $path): ?array
{
for ($attempt = 0; $attempt < 3; $attempt++) {
$retryAfter = 10;
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HEADERFUNCTION => function ($ch, $header) use (&$retryAfter) {
if (stripos($header, 'Retry-After:') === 0) {
$retryAfter = (int) trim(substr($header, 12)) ?: 10;
}
return strlen($header);
},
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false) {
throw new RuntimeException('Moradas: ' . curl_error($ch));
}
if ($status === 429) { // too many requests: wait, then retry
sleep($retryAfter);
continue;
}
if ($status === 404) { // unknown postal code or art_id
return null;
}
if ($status !== 200) {
throw new RuntimeException("Moradas: HTTP $status");
}
return json_decode($body, true);
}
throw new RuntimeException('Moradas: still rate limited after 3 attempts');
}
// 1. Suggestions as the user types (a trailing door number picks the exact CP7)
$first = moradas('/suggest?q=' . rawurlencode('av liberdade lisboa 196') . '&count=3')['suggestions'][0];
echo $first['value'], ' ', $first['data']['cp7'], "\n"; // Avenida da Liberdade, Lisboa 1250-147
// 2. Exact CP7 for a street and door number (art_id changes when the data is refreshed: never store it)
$r = moradas('/resolve?art=' . $first['data']['art_id'] . '&numero=196');
echo $r['resolved']['data']['cp7'] ?? 'no exact CP7', "\n"; // 1250-147
// 3. Postal code card (check the format first)
$cp7 = '1000-098';
if (preg_match('/^\d{4}-\d{3}$/D', $cp7)) {
$card = moradas('/cp/' . $cp7);
echo $card ? "{$card['localidade']}, {$card['concelho']}, {$card['distrito']}" : 'CP7 not found', "\n"; // Lisboa, Lisboa, Lisboa
}
# Python 3.8+ with requests (pip install requests)
import re
import time
import requests
BASE = "https://moradas.dev"
def moradas(path, params=None):
for attempt in range(3):
res = requests.get(BASE + path, params=params, timeout=10)
if res.status_code == 429: # too many requests: wait, then retry
wait = res.headers.get("Retry-After", "10")
time.sleep(int(wait) if wait.isdigit() else 10)
continue
if res.status_code == 404: # unknown postal code or art_id
return None
res.raise_for_status()
return res.json()
raise RuntimeError("Moradas: still rate limited after 3 attempts")
# 1. Suggestions as the user types (a trailing door number picks the exact CP7)
first = moradas("/suggest", {"q": "av liberdade lisboa 196", "count": 3})["suggestions"][0]
print(first["value"], first["data"]["cp7"]) # Avenida da Liberdade, Lisboa 1250-147
# 2. Exact CP7 for a street and door number (art_id changes when the data is refreshed: never store it)
r = moradas("/resolve", {"art": first["data"]["art_id"], "numero": 196})
print(r["resolved"]["data"]["cp7"] if r and r["resolved"] else "no exact CP7") # 1250-147
# 3. Postal code card (check the format first)
cp7 = "1000-098"
if re.fullmatch(r"\d{4}-\d{3}", cp7, re.ASCII):
card = moradas("/cp/" + cp7)
print(f"{card['localidade']}, {card['concelho']}, {card['distrito']}" if card else "CP7 not found") # Lisboa, Lisboa, Lisboa
// .NET 8+ console app (Program.cs)
using System.Net;
using System.Text.Json;
using System.Text.RegularExpressions;
var http = new HttpClient { BaseAddress = new Uri("https://moradas.dev") };
async Task<JsonElement?> Moradas(string path)
{
for (var attempt = 0; attempt < 3; attempt++)
{
using var res = await http.GetAsync(path);
if (res.StatusCode == HttpStatusCode.TooManyRequests) // too many requests: wait, then retry
{
await Task.Delay(res.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(10));
continue;
}
if (res.StatusCode == HttpStatusCode.NotFound) return null; // unknown postal code or art_id
res.EnsureSuccessStatusCode();
return JsonSerializer.Deserialize<JsonElement>(await res.Content.ReadAsStringAsync());
}
throw new HttpRequestException("Moradas: still rate limited after 3 attempts");
}
// 1. Suggestions as the user types (a trailing door number picks the exact CP7)
var s = await Moradas("/suggest?q=" + Uri.EscapeDataString("av liberdade lisboa 196") + "&count=3");
var first = s!.Value.GetProperty("suggestions")[0];
var data = first.GetProperty("data");
Console.WriteLine($"{first.GetProperty("value")} {data.GetProperty("cp7")}"); // Avenida da Liberdade, Lisboa 1250-147
// 2. Exact CP7 for a street and door number (art_id changes when the data is refreshed: never store it)
var r = await Moradas($"/resolve?art={data.GetProperty("art_id")}&numero=196");
var resolved = r?.GetProperty("resolved");
Console.WriteLine(resolved?.ValueKind == JsonValueKind.Object
? resolved.Value.GetProperty("data").GetProperty("cp7").GetString()
: "no exact CP7"); // 1250-147
// 3. Postal code card (check the format first)
var cp7 = "1000-098";
if (Regex.IsMatch(cp7, @"^[0-9]{4}-[0-9]{3}\z"))
{
var card = await Moradas("/cp/" + cp7);
Console.WriteLine(card is { } c
? $"{c.GetProperty("localidade")}, {c.GetProperty("concelho")}, {c.GetProperty("distrito")}"
: "CP7 not found"); // Lisboa, Lisboa, Lisboa
}
Retry-After, espere esses segundos; se não, espere cerca de 10 segundos e repita. Num campo de autocomplete não repita: ignore essa tecla, a seguinte faz um pedido novo./cp/{cp7} devolve 404 quando o código postal não existe ({"error":"CP7 not found"}) e /resolve quando o art_id não existe. O cabeçalho X-Moradas-Hint explica o motivo (por exemplo, localidade colada ao código ou um código «redondo» como 4700-000). O /suggest nunca devolve 404: sem resultados, a lista vem vazia ({"suggestions":[]})./suggest e nunca o guarde.^\d{4}-\d{3}$ (ex.: 1000-098). Valide antes de chamar /cp e poupe pedidos. Mais em Validar código postal.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âmetro | significado |
|---|---|
q | código postal (1000-098, 4–7 dígitos) ou texto livre (av liberdade lisboa 196) |
count | má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.
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.
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).
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 }
] }
/v1, /api nem /api/v1: os endpoints ficam na raiz — https://moradas.dev/suggest, /resolve, /cp/{cp7}, /cp/{cp4}. Um 404 nestes caminhos traz a indicação no cabeçalho X-Moradas-Hint.GET /suggest?q=, não /cp/suggest. O /cp/ leva só o código postal, sem localidade: /cp/1100-413, não /cp/1100-413 LISBOA.suggestions[].data em /suggest, arterias[].street em /cp. Não existem ruas, nome, rua ou results.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).
| campo | significado |
|---|---|
items[].q | a morada em texto livre, como q no /suggest: rua, número de porta e localidade, ou um código postal. Obrigatório. |
items[].id | qualquer texto ou número seu (número da encomenda, id do registo), devolvido tal e qual. Opcional; na resposta vem null quando falta. |
results[].status | ok, ambiguous ou not_found. |
results[].cp7 | o código postal quando ok; senão null. |
results[].suggestion | a 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
Uma morada não apareceu? Envie-a aqui.
POST /feedback
Content-Type: application/json
{ "query": "rua que não apareceu, localidade", "note": "optional details" }
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ção | significado |
|---|---|
fill | Campos 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. |
minChars | Caracteres escritos antes do primeiro pedido; 2 por defeito. |
debounce | Pausa depois da última tecla, em milissegundos; 160 por defeito. |
count | Máximo de sugestões na lista; 8 por defeito, limite 20. |
endpoint | URL base da API; https://moradas.dev por defeito. |
beacon | true 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.
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>
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.
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" } } }
suggest_address — autocompleta uma morada ou um código postal a partir de texto livre (q, opcionalmente count); o mesmo que o /suggest.resolve_postal_code — o CP7 exato de uma rua com vários códigos postais, pelo art_id da sugestão e o número de porta; o mesmo que o /resolve.postal_code_info — a ficha de um código postal: localidade, concelho, distrito e as ruas que abrange; o mesmo que o /cp/{cp7}.verify_addresses — verifica uma lista de moradas num só pedido (coluna de um CRM ou folha de cálculo, importação de encomendas, base de clientes antes de enviar): items com até 100 linhas {id, q}. Devolve o mesmo JSON que o POST /batch: results pela ordem de entrada, com o id tal e qual (null quando falta), status ok / ambiguous / not_found, cp7 e suggestion. Um pedido com N moradas conta como N pedidos no limite — envie até 50 de cada vez; mais de 100 linhas ou um pedido mal formado devolve {"error": "…"} com o motivo, como nas outras ferramentas. Para uma só morada use suggest_address.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
}
}
https://moradas.dev. Feedback: dev@moradas.dev