{
  "openapi": "3.1.0",
  "info": {
    "title": "Moradas API",
    "version": "1.2.0",
    "summary": "Free API for Portuguese address autocomplete and postal codes (CP7). No API key.",
    "description": "Autocomplete Portuguese streets and localities, get the exact 7-digit postal code (CP7, `NNNN-NNN`) by door number, and look up a postal code.\n\n**Usage**\n\n- Free, including commercial use. No API key, no sign-up. CORS is open.\n- Fair-use limit: about 50 requests per 10 seconds per IP. Exceeding it returns HTTP 429.\n- No uptime guarantee — cache what you need.\n- Stable contract (v1): fields are never renamed or removed, only added. Breaking changes would only ship as a separate `/v2`.\n- Status: beta.\n- There is no `/v1`, `/api` or `/api/v1` prefix: every path below is at the root of `https://moradas.dev`. A 404 carries an `X-Moradas-Hint` header that says why.\n\n**Typical flow**\n\n1. `GET /suggest?q=...` as the user types.\n2. If the chosen suggestion has `cp7: null` (the street spans several postal codes), call `GET /resolve?art={art_id}&numero={door number}` — or simply include the door number at the end of `q`.\n3. Have a list of addresses (order import, CRM, database clean-up)? `POST /batch` checks up to 100 of them in one request and gives each a verdict: `ok`, `ambiguous` or `not_found`. A batch of N items counts as N requests against the fair-use limit.\n\n`art_id` is ephemeral: it changes when the data is refreshed. Use it right after `/suggest` and never store it. The examples below are real responses from 30 September 2026; their `art_id` values will not stay valid, so always take `art_id` from a fresh `/suggest` response.\n\nField names are Portuguese: `localidade` (postal locality), `concelho` (municipality), `distrito` (district), `cp7` (postal code).\n\nAI agents can use the same data through the remote MCP server at `https://moradas.dev/mcp` (Streamable HTTP, no key) — see the docs.",
    "termsOfService": "https://moradas.dev/docs",
    "contact": {
      "name": "Moradas",
      "url": "https://moradas.dev",
      "email": "dev@moradas.dev"
    },
    "license": {
      "name": "Free to use, including commercial use (see Usage in the docs)",
      "url": "https://moradas.dev/docs"
    },
    "x-logo": {
      "url": "https://moradas.dev/apple-touch-icon.png"
    }
  },
  "externalDocs": {
    "description": "Moradas API documentation",
    "url": "https://moradas.dev/docs"
  },
  "servers": [
    {
      "url": "https://moradas.dev",
      "description": "Production"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "Addresses",
      "description": "Street and locality autocomplete, exact postal code by door number."
    },
    {
      "name": "Postal codes",
      "description": "Postal code (CP7) lookup."
    },
    {
      "name": "Feedback",
      "description": "Report an address that was not found."
    },
    {
      "name": "Service",
      "description": "Service status."
    },
    {
      "name": "Widget",
      "description": "Error reports sent by the embeddable widget (widget.js)."
    }
  ],
  "paths": {
    "/suggest": {
      "get": {
        "tags": [
          "Addresses"
        ],
        "operationId": "suggestAddress",
        "summary": "Autocomplete an address or postal code",
        "description": "One endpoint for everything a checkout needs.\n\n- **4–7 digits** (spaces and hyphens ignored, e.g. `1000-098`, `1000`) → postal-code prefix lookup.\n- **Text** (2+ characters) → streets and localities. Accents are optional (`sao joao`), abbreviations work (`av` → Avenida), prepositions are optional. Bigger cities rank first.\n- **Text ending with a door number** (`av liberdade lisboa 196`) → the number is taken as the door number when the text without it matches; each suggestion then carries `numero` and `resolved_cp7`, and `cp7` is set to the resolved code when one was found.\n\nShorter or empty queries return an empty list.\n\n`nseg` is the number of postal-code segments on the street. With `cp7: null` the street spans several postal codes — use `/resolve` with the door number.\n\n`art_id` is ephemeral: it changes when the data is refreshed. Use it right after this call and never store it.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Postal code (4–7 digits, e.g. `1000-098`) or free text: street, optionally locality and door number (e.g. `av liberdade lisboa 196`).",
            "schema": {
              "type": "string"
            },
            "examples": {
              "text": {
                "summary": "Street text",
                "value": "rua das"
              },
              "doorNumber": {
                "summary": "Street with door number",
                "value": "estrada malveira da serra 920"
              },
              "postalCode": {
                "summary": "Postal code",
                "value": "1000-098"
              }
            }
          },
          {
            "name": "count",
            "in": "query",
            "required": false,
            "description": "Maximum number of suggestions. Default 10; values above 20 are capped at 20.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 20,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Suggestions (possibly empty).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuggestResponse"
                },
                "examples": {
                  "text": {
                    "summary": "GET /suggest?q=rua das&count=3",
                    "value": {
                      "suggestions": [
                        {
                          "value": "Rua das Acácias, Lisboa",
                          "data": {
                            "art_id": 132466,
                            "kind": "street",
                            "street": "Rua das Acácias",
                            "localidade": "Lisboa",
                            "concelho": "Lisboa",
                            "distrito": "Lisboa",
                            "cp7": "1500-663",
                            "nseg": 1
                          }
                        },
                        {
                          "value": "Rua das Açucenas, Lisboa",
                          "data": {
                            "art_id": 132470,
                            "kind": "street",
                            "street": "Rua das Açucenas",
                            "localidade": "Lisboa",
                            "concelho": "Lisboa",
                            "distrito": "Lisboa",
                            "cp7": "1300-003",
                            "nseg": 1
                          }
                        },
                        {
                          "value": "Rua das Adelas, Lisboa",
                          "data": {
                            "art_id": 132467,
                            "kind": "street",
                            "street": "Rua das Adelas",
                            "localidade": "Lisboa",
                            "concelho": "Lisboa",
                            "distrito": "Lisboa",
                            "cp7": null,
                            "nseg": 2
                          }
                        }
                      ]
                    }
                  },
                  "doorNumber": {
                    "summary": "GET /suggest?q=estrada malveira da serra 920&count=2",
                    "value": {
                      "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"
                          }
                        }
                      ]
                    }
                  },
                  "postalCode": {
                    "summary": "GET /suggest?q=1000-098",
                    "value": {
                      "suggestions": [
                        {
                          "value": "Praça do Chile, Lisboa",
                          "data": {
                            "art_id": 130446,
                            "kind": "street",
                            "street": "Praça do Chile",
                            "localidade": "Lisboa",
                            "concelho": "Lisboa",
                            "distrito": "Lisboa",
                            "cp7": "1000-098",
                            "nseg": 1
                          }
                        }
                      ]
                    }
                  },
                  "locality": {
                    "summary": "GET /suggest?q=almoster&count=3 — localities without streets (kind loc) and a street with several postal codes",
                    "value": {
                      "suggestions": [
                        {
                          "value": "Almoster",
                          "data": {
                            "art_id": 106759,
                            "kind": "loc",
                            "street": "",
                            "localidade": "Almoster",
                            "concelho": "Alvaiázere",
                            "distrito": "Leiria",
                            "cp7": "3250-021",
                            "nseg": 1
                          }
                        },
                        {
                          "value": "Almoster",
                          "data": {
                            "art_id": 106760,
                            "kind": "loc",
                            "street": "",
                            "localidade": "Almoster",
                            "concelho": "Alvaiázere",
                            "distrito": "Leiria",
                            "cp7": "3254-601",
                            "nseg": 1
                          }
                        },
                        {
                          "value": "Rua Conde de Almoster, Lisboa",
                          "data": {
                            "art_id": 131015,
                            "kind": "street",
                            "street": "Rua Conde de Almoster",
                            "localidade": "Lisboa",
                            "concelho": "Lisboa",
                            "distrito": "Lisboa",
                            "cp7": null,
                            "nseg": 6
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/resolve": {
      "get": {
        "tags": [
          "Addresses"
        ],
        "operationId": "resolvePostalCode",
        "summary": "Exact postal code for a street and door number",
        "description": "When a street has several postal codes (`cp7: null`, `nseg > 1` in `/suggest`), picks the CP7 by door number — parity and number ranges decide. Also returns the list of postal-code segments of the street, so a user can choose when the door number does not decide.\n\n`resolved` is `null` when `numero` is missing or does not match a single postal code.\n\n`art_id` is ephemeral: it changes when the data is refreshed. Take it from a fresh `/suggest` response and never store it.",
        "parameters": [
          {
            "name": "art",
            "in": "query",
            "required": true,
            "description": "`art_id` from a fresh `/suggest` response.",
            "schema": {
              "type": "integer"
            },
            "example": 132467
          },
          {
            "name": "numero",
            "in": "query",
            "required": false,
            "description": "Door number, e.g. `10` or `27A` (the first run of digits is used).",
            "schema": {
              "type": "string"
            },
            "example": "10"
          }
        ],
        "responses": {
          "200": {
            "description": "Resolved address (or null) and the street's postal-code segments.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResolveResponse"
                },
                "examples": {
                  "resolved": {
                    "summary": "GET /resolve?art=132467&numero=10",
                    "value": {
                      "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"
                        }
                      ]
                    }
                  },
                  "noNumber": {
                    "summary": "GET /resolve?art=132467 (no door number)",
                    "value": {
                      "resolved": null,
                      "segments": [
                        {
                          "cp7": "1200-007",
                          "label": "Impares de 3 a 17A"
                        },
                        {
                          "cp7": "1200-008",
                          "label": "Pares de 2 a 28"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown `art_id` (for example, taken from a response before a data refresh).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "unknown artery"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/cp/{cp7}": {
      "get": {
        "tags": [
          "Postal codes"
        ],
        "operationId": "getPostalCode",
        "summary": "Look up a postal code (CP7) or list the codes of an area (CP4)",
        "description": "Card for one postal code: locality, municipality, district and the streets it covers (`arterias[].street`). Accepts `NNNN-NNN` or 7 digits without the hyphen.\n\nWith only the first four digits (`/cp/4700`), the response lists every CP7 in that area instead, in order, each with its locality, municipality and district (the same ones the card returns) and `cliente` (the organisation's name when the code belongs to it alone, otherwise `null`). Large areas have close to a thousand codes; cache the list.\n\nThe path takes the postal code alone: `/cp/1100-413`, not `/cp/1100-413 LISBOA`. On a 404 the body does not change, and the `X-Moradas-Hint` header says why.",
        "parameters": [
          {
            "name": "cp7",
            "in": "path",
            "required": true,
            "description": "Postal code, `NNNN-NNN` or `NNNNNNN`; or the first four digits `NNNN` for the list of codes in that area.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}(-?\\d{3})?$"
            },
            "examples": {
              "cp7": {
                "summary": "Postal code",
                "value": "1000-098"
              },
              "cp4": {
                "summary": "Area (first four digits)",
                "value": "4700"
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Postal code card (`/cp/{cp7}`) or the list of codes in an area (`/cp/{cp4}`).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/PostalCode"
                    },
                    {
                      "$ref": "#/components/schemas/PostalArea"
                    }
                  ]
                },
                "examples": {
                  "cp7": {
                    "summary": "GET /cp/1000-098",
                    "value": {
                      "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
                        }
                      ]
                    }
                  },
                  "cp4": {
                    "summary": "GET /cp/4700 (shortened here to the first three codes)",
                    "value": {
                      "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
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Postal code or area not found (`CP7 not found`) or malformed path (`not found`). The body is always one of these two; the `X-Moradas-Hint` header explains the cause.",
            "headers": {
              "X-Moradas-Hint": {
                "$ref": "#/components/headers/Hint"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "notFound": {
                    "summary": "GET /cp/4700-000 — X-Moradas-Hint: 4700-000 is not an assigned postal code; /cp/4700 lists the postal codes that start with 4700",
                    "value": {
                      "error": "CP7 not found"
                    }
                  },
                  "malformed": {
                    "summary": "GET /cp/1100-413%20LISBOA — X-Moradas-Hint: Use only the postal code, without locality or other text: /cp/1100-413",
                    "value": {
                      "error": "not found"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/batch": {
      "post": {
        "tags": [
          "Addresses"
        ],
        "operationId": "batchCheck",
        "summary": "Check a list of addresses in one request",
        "description": "For programs that hold a list of addresses — an order import, a CRM, a customer database to clean up. Up to 100 items per request, each checked with the same logic as `/suggest` (a trailing door number picks the exact CP7; a postal code is looked up as such). Results come back in input order, one per item:\n\n- `ok` — a postal code was determined: `cp7` is set and `suggestion` is the matching street (the same object `/suggest` returns, with `resolved_cp7` when a door number decided it).\n- `ambiguous` — no single answer: several candidates with nothing to tell them apart (the query names no locality, or the same street exists in two localities), or a street that spans several postal codes and the query has no door number, or just a locality or municipality. `cp7` is `null`; `suggestion` is the best candidate, for a human to check.\n- `not_found` — nothing matched. `cp7` and `suggestion` are `null`.\n\nVerdict rule for `ok`: the first suggestion has a postal code (`resolved_cp7`, else `cp7`) and is a clear leader — it is the only candidate, or every candidate yields the same code, or the door number matched it alone, or it covers more words of the query than any other candidate (`rua das flores dona maria` → Dona Maria, not Lisboa).\n\n**Rate limit:** a batch of N items counts as N requests against the fair-use limit (about 50 requests per 10 seconds per IP). A batch larger than the remaining allowance is rejected as a whole with 429 and `Retry-After` — in practice send up to 50 items per call and wait for `Retry-After` on 429. Responses are not cached. `id` is any string or number of yours, echoed back unchanged (`null` when not sent); `q` is echoed back trimmed and cut at 500 characters.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchRequest"
              },
              "example": {
                "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"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One result per item, in input order (a real response from 2 October 2026; `art_id` values change with every data refresh).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchResponse"
                },
                "example": {
                  "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
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON, `items` is not an array, an item is not an object with a string `q`, or there are more than 100 items. The `hint` says what to send.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalidJson": {
                    "summary": "Body is not JSON",
                    "value": {
                      "error": "invalid JSON",
                      "hint": "POST a JSON body {\"items\":[{\"id\":\"your id\",\"q\":\"street, door number, locality\"}]} with up to 100 items (64 KB). Docs: https://moradas.dev/docs#batch"
                    }
                  },
                  "tooMany": {
                    "summary": "101 items",
                    "value": {
                      "error": "too many items: 101 (max 100)",
                      "hint": "Split the list into batches of up to 100 items. Docs: https://moradas.dev/docs#batch"
                    }
                  },
                  "badItem": {
                    "summary": "An item without a string q",
                    "value": {
                      "error": "items[1].q must be a string",
                      "hint": "POST a JSON body {\"items\":[{\"id\":\"your id\",\"q\":\"street, door number, locality\"}]} with up to 100 items (64 KB). Docs: https://moradas.dev/docs#batch"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "Only `POST` is accepted (`GET /batch` returns this); the `X-Moradas-Hint` header says what to send and `Allow` lists the methods.",
            "headers": {
              "X-Moradas-Hint": {
                "$ref": "#/components/headers/Hint"
              },
              "Allow": {
                "schema": {
                  "type": "string"
                },
                "description": "`POST, OPTIONS`"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "method not allowed"
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "body too large (max 64 KB)",
                  "hint": "POST a JSON body {\"items\":[{\"id\":\"your id\",\"q\":\"street, door number, locality\"}]} with up to 100 items (64 KB). Docs: https://moradas.dev/docs#batch"
                }
              }
            }
          },
          "429": {
            "description": "Fair-use limit exceeded. A batch of N items counts as N requests (about 50 per 10 seconds per IP), so a batch larger than the remaining allowance is rejected as a whole; nothing in it is processed. Wait for `Retry-After` seconds and resend. Very high request rates may be rejected at the network edge with a plain-text body.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait before retrying (10). Readable from browser JavaScript (`Access-Control-Expose-Headers`)."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "rate limited — be gentle, this is a free service"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/feedback": {
      "post": {
        "tags": [
          "Feedback"
        ],
        "operationId": "sendFeedback",
        "summary": "Report an address that was not found",
        "description": "An address didn't come up? Send it here.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FeedbackRequest"
              },
              "example": {
                "query": "rua que não apareceu, localidade",
                "note": "optional details"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Feedback stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeedbackResponse"
                },
                "example": {
                  "ok": true,
                  "obrigado": true
                }
              }
            }
          },
          "400": {
            "description": "`query` is missing or empty.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "query required"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/widget-error": {
      "post": {
        "tags": [
          "Widget"
        ],
        "operationId": "reportWidgetError",
        "summary": "Error report from the widget",
        "description": "Sent by `widget.js` with `navigator.sendBeacon` when the service does not answer properly (HTTP 429, 5xx, a network error or an unexpected reply): at most one report per error kind and page load. The widget option `beacon: false` turns it off.\n\nOnly hourly counts are kept, per site (the host name from the `Origin` or `Referer` header that the browser adds), error kind, HTTP status and widget version. No address, typed text, page path or IP address is stored. Integrations that do not use the widget have no reason to call this endpoint.",
        "requestBody": {
          "required": true,
          "content": {
            "text/plain": {
              "schema": {
                "type": "string",
                "description": "JSON text of a `WidgetErrorReport` (sent as `text/plain` so that browsers need no CORS preflight)."
              },
              "example": "{\"kind\":\"429\",\"widget_version\":\"0.3.0\",\"http_status\":429}"
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WidgetErrorReport"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Report counted."
          },
          "400": {
            "description": "The body is not a report of a known kind.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "expected JSON {kind, widget_version, http_status}"
                }
              }
            }
          },
          "429": {
            "description": "More than 10 reports in 10 seconds from one IP address (no body)."
          }
        }
      }
    },
    "/health": {
      "get": {
        "tags": [
          "Service"
        ],
        "operationId": "getHealth",
        "summary": "Service status",
        "description": "Returns `ok: true` when the service and its data are available.",
        "responses": {
          "200": {
            "description": "Service is up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          },
          "429": {
            "description": "Rejected at the network edge under very high request rates (plain-text body). `/health` is not subject to the per-IP API limit.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Suggestion": {
        "type": "object",
        "required": [
          "value",
          "data"
        ],
        "properties": {
          "value": {
            "type": "string",
            "description": "Display label: `street, localidade`, or just `localidade` for kind `loc`. In postal-code lookups with an organisation-specific code, prefixed with `cliente — `.",
            "examples": [
              "Avenida da Liberdade, Lisboa"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/Address"
          }
        }
      },
      "Address": {
        "type": "object",
        "required": [
          "art_id",
          "kind",
          "street",
          "localidade",
          "concelho",
          "distrito",
          "cp7",
          "nseg"
        ],
        "properties": {
          "art_id": {
            "type": "integer",
            "description": "Street/locality id for `/resolve`. Ephemeral: changes when the data is refreshed — use right away, never store. 0 means a whole locality or municipality (kind \"loc\", cp7 null): there is no street to resolve."
          },
          "kind": {
            "type": "string",
            "enum": [
              "street",
              "loc"
            ],
            "description": "`street` — a street; `loc` — a locality or postal code without a street name."
          },
          "street": {
            "type": "string",
            "description": "Street name; empty string for kind `loc`."
          },
          "localidade": {
            "type": "string",
            "description": "Postal locality."
          },
          "concelho": {
            "type": [
              "string",
              "null"
            ],
            "description": "Municipality."
          },
          "distrito": {
            "type": [
              "string",
              "null"
            ],
            "description": "District."
          },
          "cp7": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d{4}-\\d{3}$",
            "description": "Postal code `NNNN-NNN`. `null` when the street spans several postal codes and no door number decided it — use `/resolve`."
          },
          "nseg": {
            "type": "integer",
            "description": "Number of postal-code segments on the street."
          },
          "numero": {
            "type": "string",
            "description": "Door number detected at the end of `q`. Present only in `/suggest` text results when a door number was detected."
          },
          "resolved_cp7": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d{4}-\\d{3}$",
            "description": "Postal code picked by the detected door number, or `null` if it did not match a single code. Present together with `numero`."
          },
          "cliente": {
            "type": "string",
            "description": "Name of the organisation that has its own dedicated postal code. Present only in postal-code lookups for such codes."
          },
          "corrected": {
            "type": "boolean",
            "description": "Present and true only when the result was found after correcting a typo in the query (for example 'rua agusta' → Rua Augusta). Absent otherwise."
          }
        }
      },
      "SuggestResponse": {
        "type": "object",
        "required": [
          "suggestions"
        ],
        "properties": {
          "suggestions": {
            "type": "array",
            "maxItems": 20,
            "items": {
              "$ref": "#/components/schemas/Suggestion"
            }
          }
        }
      },
      "BatchRequest": {
        "type": "object",
        "required": [
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "maxItems": 100,
            "items": {
              "$ref": "#/components/schemas/BatchItem"
            },
            "description": "Up to 100 addresses. The whole body must stay under 64 KB."
          }
        }
      },
      "BatchItem": {
        "type": "object",
        "required": [
          "q"
        ],
        "properties": {
          "id": {
            "type": [
              "string",
              "number"
            ],
            "description": "Your own identifier for the row (order number, record id), echoed back unchanged. Optional."
          },
          "q": {
            "type": "string",
            "description": "The address as free text, like `q` in `/suggest`: street, door number and locality (`Avenida da Liberdade 196, Lisboa`), or a postal code. A number is accepted and treated as its digits.",
            "maxLength": 500
          }
        }
      },
      "BatchResponse": {
        "type": "object",
        "required": [
          "results"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BatchResult"
            },
            "description": "One result per item, in the order of the request."
          }
        }
      },
      "BatchResult": {
        "type": "object",
        "required": [
          "id",
          "q",
          "status",
          "cp7",
          "suggestion"
        ],
        "properties": {
          "id": {
            "type": [
              "string",
              "number",
              "null"
            ],
            "description": "The `id` sent with the item; `null` when none was sent."
          },
          "q": {
            "type": "string",
            "description": "The query as sent, trimmed and cut at 500 characters."
          },
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "ambiguous",
              "not_found"
            ],
            "description": "`ok` — postal code determined (`cp7` set); `ambiguous` — several candidates, or a street with several postal codes and no door number, or just a locality; `not_found` — nothing matched."
          },
          "cp7": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d{4}-\\d{3}$",
            "description": "The postal code for `ok`; `null` otherwise."
          },
          "suggestion": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Suggestion"
              },
              {
                "type": "null"
              }
            ],
            "description": "The first suggestion, exactly as `/suggest` returns it (with `numero`/`resolved_cp7` when a door number was detected, `corrected: true` after a typo fix); for `ambiguous` the best candidate; `null` for `not_found`."
          }
        }
      },
      "Segment": {
        "type": "object",
        "required": [
          "cp7",
          "label"
        ],
        "properties": {
          "cp7": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{3}$"
          },
          "label": {
            "type": "string",
            "description": "Human-readable segment rule (door-number range and parity, door, sub-locality, organisation), separated by `; `. Empty when the segment has no restriction.",
            "examples": [
              "Impares de 3 a 17A"
            ]
          }
        }
      },
      "ResolveResponse": {
        "type": "object",
        "required": [
          "resolved",
          "segments"
        ],
        "properties": {
          "resolved": {
            "description": "The street with `data.cp7` set to the postal code for the door number, or `null`.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/Suggestion"
              },
              {
                "type": "null"
              }
            ]
          },
          "segments": {
            "type": "array",
            "description": "All postal-code segments of the street, ordered by postal code.",
            "items": {
              "$ref": "#/components/schemas/Segment"
            }
          }
        }
      },
      "PostalCodeStreet": {
        "type": "object",
        "required": [
          "art_id",
          "street",
          "troco",
          "porta",
          "cliente"
        ],
        "properties": {
          "art_id": {
            "type": "integer",
            "description": "Street id. Ephemeral: changes when the data is refreshed — never store."
          },
          "street": {
            "type": "string"
          },
          "troco": {
            "type": [
              "string",
              "null"
            ],
            "description": "Door-number range covered by this postal code, e.g. `Pares de 2 a 28`."
          },
          "porta": {
            "type": [
              "string",
              "null"
            ],
            "description": "Specific door number covered by this postal code."
          },
          "cliente": {
            "type": [
              "string",
              "null"
            ],
            "description": "Organisation that has this postal code as its own."
          }
        }
      },
      "PostalCode": {
        "type": "object",
        "required": [
          "cp7",
          "cp4",
          "cp3",
          "distrito",
          "concelho",
          "localidade",
          "arterias"
        ],
        "properties": {
          "cp7": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{3}$"
          },
          "cp4": {
            "type": "string",
            "pattern": "^\\d{4}$"
          },
          "cp3": {
            "type": "string",
            "pattern": "^\\d{3}$"
          },
          "distrito": {
            "type": [
              "string",
              "null"
            ]
          },
          "concelho": {
            "type": [
              "string",
              "null"
            ]
          },
          "localidade": {
            "type": "string"
          },
          "arterias": {
            "type": "array",
            "description": "Streets covered by the postal code. Empty for postal codes without a street.",
            "items": {
              "$ref": "#/components/schemas/PostalCodeStreet"
            }
          }
        }
      },
      "PostalAreaCode": {
        "type": "object",
        "required": [
          "cp7",
          "localidade",
          "concelho",
          "distrito",
          "cliente"
        ],
        "properties": {
          "cp7": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{3}$"
          },
          "localidade": {
            "type": "string",
            "description": "Postal locality (the same as in `/cp/{cp7}`)."
          },
          "concelho": {
            "type": [
              "string",
              "null"
            ]
          },
          "distrito": {
            "type": [
              "string",
              "null"
            ]
          },
          "cliente": {
            "type": [
              "string",
              "null"
            ],
            "description": "Organisation that has this postal code to itself; `null` for an ordinary code."
          }
        }
      },
      "PostalArea": {
        "type": "object",
        "required": [
          "cp4",
          "codigos"
        ],
        "properties": {
          "cp4": {
            "type": "string",
            "pattern": "^\\d{4}$"
          },
          "codigos": {
            "type": "array",
            "description": "Every postal code that starts with `cp4`, ordered by code.",
            "items": {
              "$ref": "#/components/schemas/PostalAreaCode"
            }
          }
        }
      },
      "WidgetErrorReport": {
        "type": "object",
        "required": [
          "kind"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "429",
              "5xx",
              "network",
              "json",
              "http"
            ],
            "description": "`429` rate limit, `5xx` server failure, `network` no answer, `json` unexpected reply, `http` another HTTP error."
          },
          "widget_version": {
            "type": "string",
            "examples": [
              "0.3.0"
            ]
          },
          "http_status": {
            "type": [
              "integer",
              "null"
            ],
            "description": "HTTP status of the failed request; `null` for a network error."
          }
        }
      },
      "FeedbackRequest": {
        "type": "object",
        "required": [
          "query"
        ],
        "properties": {
          "query": {
            "type": "string",
            "minLength": 1,
            "description": "The address that was not found. Longer text is truncated to 500 characters."
          },
          "note": {
            "type": "string",
            "description": "Optional details. Longer text is truncated to 1000 characters."
          }
        }
      },
      "FeedbackResponse": {
        "type": "object",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "obrigado": {
            "type": "boolean",
            "const": true
          }
        }
      },
      "Health": {
        "type": "object",
        "required": [
          "ok",
          "arteries"
        ],
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "arteries": {
            "type": "integer",
            "description": "Number of streets and localities currently indexed."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "detail": {
            "type": "string",
            "description": "Present on internal errors."
          },
          "hint": {
            "type": "string",
            "description": "Present on `/batch` errors: what to send instead."
          }
        }
      }
    },
    "headers": {
      "Hint": {
        "description": "Why nothing was found, in English (ASCII), e.g. `Postal code 9999-999 does not exist, and no postal codes start with 9999`. Readable from browser JavaScript (`Access-Control-Expose-Headers`).",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "RateLimited": {
        "description": "Fair-use limit exceeded (about 50 requests per 10 seconds per IP). Slow down and retry later. Usually a JSON body; very high request rates may be rejected at the network edge with a plain-text body.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "rate limited — be gentle, this is a free service"
            }
          },
          "text/plain": {
            "schema": {
              "type": "string"
            }
          }
        }
      },
      "InternalError": {
        "description": "Internal error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "internal",
              "detail": "…"
            }
          }
        }
      }
    }
  }
}
