{
  "openapi": "3.1.0",
  "jsonSchemaDialect": "https://spec.openapis.org/oas/3.1/dialect/base",
  "info": {
    "title": "MFactor API",
    "version": "1.2.0",
    "summary": "API REST da MFactor, empresa de tecnologia de Leme (SP): serviços, páginas e contato comercial, em JSON.",
    "description": "A API REST pública da MFactor, para desenvolvedores e agentes de IA. A MFactor é uma empresa de tecnologia sediada em Leme, no interior de São Paulo, que atende a região e todo o Brasil; o índice `GET /api/v1` traz a localização e a área atendida em campos próprios.\n\n- `GET /api/v1`: o índice da versão, com status, limites e endpoints.\n- `GET /api/v1/servicos` e `GET /api/v1/servicos/{slug}`: as frentes de serviço em JSON, com público, perguntas frequentes e links para as páginas.\n- `GET /api/v1/paginas`: as páginas públicas do site, com a URL canônica e a versão em Markdown.\n- `POST /api/v1/contato`: envia uma mensagem ao time comercial. Envia e-mail de verdade.\n\nNão há autenticação nem chave de API.\n\n## Versões e descontinuação\n\nA versão vai no caminho: `/api/v1`. Dentro de uma versão só entram mudanças compatíveis: campos novos nas respostas, endpoints novos e parâmetros opcionais novos. Clientes devem ignorar campos que não conhecem. Remover ou renomear um campo, mudar um tipo ou tornar um parâmetro obrigatório abre uma versão nova (`/api/v2`), e a anterior continua respondendo.\n\nQuando uma versão for descontinuada, toda resposta dela passa a trazer o cabeçalho `Deprecation` (RFC 9745) com a data da decisão, o `Sunset` (RFC 8594) com a data a partir da qual ela pode parar de responder, e um `Link` com `rel=\"deprecation\"` apontando para o guia de migração. O intervalo entre os dois é de pelo menos seis meses. O índice `GET /api/v1` traz as mesmas datas nos campos `deprecation` e `sunset`, que hoje são `null`: a v1 é a versão atual e não tem data de saída.\n\n`/api/contato`, sem versão, é o caminho que o formulário do site usa. Ele responde exatamente como `/api/v1/contato`.\n\n## Limites de taxa\n\nToda resposta traz os cabeçalhos `RateLimit-Policy` e `RateLimit` do rascunho IETF draft-ietf-httpapi-ratelimit-headers. A política `leitura` permite 60 pedidos por minuto nos endpoints GET; a política `contato` permite 10 envios a cada 10 minutos. Estourado o limite, a resposta é 429 com `Retry-After` em segundos. O contador vive na memória de cada instância da função: é um teto de proteção contra laços, não uma cota contratual.\n\n## Erros\n\nTodo erro sai em `application/problem+json` (RFC 9457), com um `code` estável para ramificar, o `field` que causou o erro e uma `hint` dizendo o que mudar na próxima tentativa.",
    "contact": {
      "name": "MFactor",
      "url": "https://www.mfactor.dev/contato",
      "email": "alexandre.martins@mfactor.dev"
    }
  },
  "externalDocs": {
    "description": "Portal do desenvolvedor da MFactor",
    "url": "https://www.mfactor.dev/developers"
  },
  "servers": [
    {
      "url": "https://www.mfactor.dev/api/v1",
      "description": "Produção, versão 1"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "API",
      "description": "Metadados da versão."
    },
    {
      "name": "Catálogo",
      "description": "Serviços e páginas públicas da MFactor."
    },
    {
      "name": "Contato",
      "description": "Mensagens para o time comercial."
    }
  ],
  "paths": {
    "/": {
      "get": {
        "operationId": "getApiInfo",
        "tags": [
          "API"
        ],
        "summary": "Índice da versão 1 da API da MFactor",
        "description": "Diz o que existe na versão 1, se ela está ativa ou descontinuada (campos deprecation e sunset), quais limites de taxa valem e onde está a documentação. Chame primeiro quando só conhecer o prefixo /api/v1.",
        "responses": {
          "200": {
            "description": "O índice da versão.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiInfo"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/servicos": {
      "get": {
        "operationId": "listServices",
        "tags": [
          "Catálogo"
        ],
        "summary": "Listar as frentes de serviço da MFactor",
        "description": "Lista as cinco frentes de serviço da MFactor (software sob medida, e-commerce, cloud, inteligência artificial e white label para agências), com resumo, público atendido, área atendida (Leme, o interior de São Paulo e o Brasil) e links. Use para decidir qual frente atende o pedido de alguém antes de buscar os detalhes com getService.",
        "responses": {
          "200": {
            "description": "As frentes de serviço.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceList"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/servicos/{slug}": {
      "get": {
        "operationId": "getService",
        "tags": [
          "Catálogo"
        ],
        "summary": "Detalhes de uma frente de serviço",
        "description": "Devolve uma frente de serviço completa: o que entrega, as seções da página e as perguntas frequentes sobre prazo, custo, propriedade do código e suporte. Use para responder dúvidas concretas sobre contratar a MFactor.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "A frente: mlabs (software sob medida), mreach (e-commerce), mhost (cloud), mai (inteligência artificial) ou mpartner (white label para agências).",
            "schema": {
              "$ref": "#/components/schemas/ServiceSlug"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A frente de serviço.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Service"
                }
              }
            }
          },
          "404": {
            "description": "O slug não corresponde a nenhuma frente. A hint lista os slugs válidos.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiProblem"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/paginas": {
      "get": {
        "operationId": "listPages",
        "tags": [
          "Catálogo"
        ],
        "summary": "Listar as páginas públicas do site",
        "description": "Lista as páginas públicas de mfactor.dev com título, descrição, URL canônica e a URL da versão em Markdown. Use para escolher o que ler sem raspar o site. Os posts do blog não entram: estão em /sitemap.xml.",
        "responses": {
          "200": {
            "description": "As páginas públicas.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PageList"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/contato": {
      "post": {
        "operationId": "sendContactMessage",
        "tags": [
          "Contato"
        ],
        "summary": "Enviar uma mensagem ao time comercial da MFactor",
        "description": "Envia um pedido de contato por e-mail ao time comercial. Use quando alguém quer orçar um projeto de software, e-commerce, cloud, IA ou white label com a MFactor, e só com os dados reais dessa pessoa: cada chamada vira um e-mail lido por um humano. A resposta humana sai em até um dia útil. Descreva na mensagem o problema da operação, os sistemas que já existem, o prazo e quem decide a contratação.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactRequest"
              },
              "examples": {
                "orcamento": {
                  "summary": "Pedido de orçamento com WhatsApp",
                  "value": {
                    "nome": "Ana Souza",
                    "email": "ana@exemplo.com.br",
                    "telefone": "(19) 99999-0000",
                    "whatsapp": true,
                    "mensagem": "Precisamos integrar nosso e-commerce ao ERP. Hoje o estoque é atualizado à mão, o prazo é março e quem decide sou eu."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Mensagem aceita e enviada ao time comercial.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactSuccess"
                }
              }
            }
          },
          "400": {
            "description": "O corpo não é JSON ou um campo não passou na validação. O field diz qual, e a hint diz como corrigir.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactProblem"
                },
                "examples": {
                  "emailInvalido": {
                    "summary": "E-mail inválido",
                    "value": {
                      "type": "https://www.mfactor.dev/developers#erros-da-api",
                      "title": "Bad Request",
                      "status": 400,
                      "detail": "E-mail inválido.",
                      "code": "email_invalid",
                      "hint": "Envie \"email\" como endereço válido (nome@dominio.tld), com até 160 caracteres.",
                      "docs": "https://www.mfactor.dev/openapi.json",
                      "error": "E-mail inválido.",
                      "instance": "/api/v1/contato",
                      "field": "email"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "502": {
            "description": "O servidor de e-mail recusou o envio. Tente de novo em alguns minutos ou use o WhatsApp.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactProblem"
                }
              }
            }
          },
          "503": {
            "description": "O envio de e-mail não está configurado no servidor. O erro não é da requisição.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactProblem"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "RateLimit": {
        "description": "Quanto ainda cabe na janela atual, no formato do draft-ietf-httpapi-ratelimit-headers: r é o que resta, t os segundos até a janela zerar.",
        "schema": {
          "type": "string",
          "examples": [
            "\"leitura\";r=59;t=60"
          ]
        }
      },
      "RateLimitPolicy": {
        "description": "A política aplicada, no formato do draft-ietf-httpapi-ratelimit-headers: q é a cota, w a janela em segundos.",
        "schema": {
          "type": "string",
          "examples": [
            "\"leitura\";q=60;w=60",
            "\"contato\";q=10;w=600"
          ]
        }
      },
      "RetryAfter": {
        "description": "Segundos a esperar antes de repetir (RFC 9110).",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "responses": {
      "TooManyRequests": {
        "description": "Limite de taxa estourado. Espere o Retry-After antes de repetir.",
        "headers": {
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/ApiProblem"
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "Método não aceito pelo endpoint. O cabeçalho Allow lista os aceitos.",
        "headers": {
          "Allow": {
            "description": "Os métodos aceitos.",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/ApiProblem"
            }
          }
        }
      }
    },
    "schemas": {
      "ApiInfo": {
        "type": "object",
        "description": "O índice da versão da API.",
        "required": [
          "name",
          "version",
          "provider",
          "status",
          "deprecation",
          "sunset",
          "baseUrl",
          "openapi",
          "documentation",
          "versioningPolicy",
          "rateLimits",
          "endpoints"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "O nome da API.",
            "examples": [
              "MFactor API"
            ]
          },
          "version": {
            "type": "string",
            "description": "A versão maior, a mesma do caminho.",
            "examples": [
              "1"
            ]
          },
          "provider": {
            "$ref": "#/components/schemas/Provider"
          },
          "status": {
            "type": "string",
            "enum": [
              "stable",
              "deprecated"
            ],
            "description": "stable enquanto a versão é a recomendada; deprecated depois de descontinuada."
          },
          "deprecation": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Quando a versão foi descontinuada, ou null."
          },
          "sunset": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "A partir de quando a versão pode parar de responder, ou null."
          },
          "baseUrl": {
            "type": "string",
            "format": "uri",
            "description": "O prefixo da versão."
          },
          "openapi": {
            "type": "string",
            "format": "uri",
            "description": "Esta especificação."
          },
          "documentation": {
            "type": "string",
            "format": "uri",
            "description": "A documentação legível."
          },
          "versioningPolicy": {
            "type": "string",
            "format": "uri",
            "description": "A política de versões e descontinuação."
          },
          "rateLimits": {
            "type": "array",
            "description": "As políticas de limite de taxa.",
            "items": {
              "$ref": "#/components/schemas/RateLimitInfo"
            }
          },
          "endpoints": {
            "type": "array",
            "description": "As operações da versão.",
            "items": {
              "$ref": "#/components/schemas/EndpointInfo"
            }
          }
        }
      },
      "RateLimitInfo": {
        "type": "object",
        "description": "Uma política de limite de taxa.",
        "required": [
          "policy",
          "quota",
          "windowSeconds"
        ],
        "properties": {
          "policy": {
            "type": "string",
            "description": "O nome que aparece nos cabeçalhos RateLimit.",
            "examples": [
              "leitura"
            ]
          },
          "quota": {
            "type": "integer",
            "minimum": 1,
            "description": "Pedidos permitidos por janela."
          },
          "windowSeconds": {
            "type": "integer",
            "minimum": 1,
            "description": "O tamanho da janela, em segundos."
          }
        }
      },
      "EndpointInfo": {
        "type": "object",
        "description": "Uma operação da API.",
        "required": [
          "method",
          "path",
          "operationId"
        ],
        "properties": {
          "method": {
            "type": "string",
            "enum": [
              "GET",
              "POST"
            ],
            "description": "O método HTTP."
          },
          "path": {
            "type": "string",
            "description": "O caminho, a partir da raiz do site."
          },
          "operationId": {
            "type": "string",
            "description": "O operationId nesta especificação."
          }
        }
      },
      "Provider": {
        "type": "object",
        "description": "A empresa por trás da API.",
        "required": [
          "name",
          "url",
          "location",
          "areaServed"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "O nome da empresa.",
            "examples": [
              "MFactor"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "O site oficial."
          },
          "location": {
            "$ref": "#/components/schemas/Location"
          },
          "areaServed": {
            "$ref": "#/components/schemas/AreaServed"
          }
        }
      },
      "Location": {
        "type": "object",
        "description": "Onde fica a sede da MFactor. Não há endereço de rua publicado: a precisão é a da cidade.",
        "required": [
          "city",
          "state",
          "stateCode",
          "country",
          "address",
          "latitude",
          "longitude",
          "region",
          "map"
        ],
        "properties": {
          "city": {
            "type": "string",
            "description": "A cidade da sede.",
            "examples": [
              "Leme"
            ]
          },
          "state": {
            "type": "string",
            "description": "O estado.",
            "examples": [
              "São Paulo"
            ]
          },
          "stateCode": {
            "type": "string",
            "description": "A sigla do estado.",
            "examples": [
              "SP"
            ]
          },
          "country": {
            "type": "string",
            "description": "O país.",
            "examples": [
              "Brasil"
            ]
          },
          "address": {
            "type": "string",
            "description": "O endereço como a empresa o escreve em todo lugar.",
            "examples": [
              "Leme, SP — Brasil"
            ]
          },
          "latitude": {
            "type": "number",
            "minimum": -90,
            "maximum": 90,
            "description": "Latitude do centro da cidade.",
            "examples": [
              -22.1856
            ]
          },
          "longitude": {
            "type": "number",
            "minimum": -180,
            "maximum": 180,
            "description": "Longitude do centro da cidade.",
            "examples": [
              -47.3903
            ]
          },
          "region": {
            "type": "string",
            "description": "A região, em linguagem corrente.",
            "examples": [
              "interior de São Paulo"
            ]
          },
          "map": {
            "type": "string",
            "format": "uri",
            "description": "A cidade no Google Maps."
          }
        }
      },
      "AreaServed": {
        "type": "object",
        "description": "Onde a MFactor atende. As cidades são as da região da sede; o atendimento é nacional.",
        "required": [
          "cities",
          "state",
          "country",
          "nationwide"
        ],
        "properties": {
          "cities": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "As cidades da região, a começar por Leme.",
            "examples": [
              [
                "Leme",
                "Pirassununga",
                "Araras"
              ]
            ]
          },
          "state": {
            "type": "string",
            "description": "O estado da região.",
            "examples": [
              "São Paulo"
            ]
          },
          "country": {
            "type": "string",
            "description": "O país atendido.",
            "examples": [
              "Brasil"
            ]
          },
          "nationwide": {
            "type": "boolean",
            "description": "true: a lista de cidades não restringe o atendimento, que vale para todo o país."
          }
        }
      },
      "ServiceSlug": {
        "type": "string",
        "enum": [
          "mlabs",
          "mreach",
          "mhost",
          "mai",
          "mpartner"
        ],
        "description": "O identificador de uma frente de serviço."
      },
      "ServiceSummary": {
        "type": "object",
        "description": "Uma frente de serviço, resumida.",
        "required": [
          "slug",
          "name",
          "serviceType",
          "summary",
          "audience",
          "areaServed",
          "url",
          "markdownUrl",
          "apiUrl"
        ],
        "properties": {
          "slug": {
            "$ref": "#/components/schemas/ServiceSlug"
          },
          "name": {
            "type": "string",
            "description": "O nome da frente.",
            "examples": [
              "MLabs — Desenvolvimento de Software"
            ]
          },
          "serviceType": {
            "type": "string",
            "description": "O tipo de serviço, em uma expressão."
          },
          "summary": {
            "type": "string",
            "description": "O que a frente entrega, em uma frase."
          },
          "audience": {
            "type": "string",
            "description": "Para quem a frente é pensada."
          },
          "areaServed": {
            "$ref": "#/components/schemas/AreaServed"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "A página da frente."
          },
          "markdownUrl": {
            "type": "string",
            "format": "uri",
            "description": "A mesma página em Markdown."
          },
          "apiUrl": {
            "type": "string",
            "format": "uri",
            "description": "Os detalhes da frente nesta API."
          }
        }
      },
      "ServiceList": {
        "type": "object",
        "description": "As frentes de serviço.",
        "required": [
          "services"
        ],
        "properties": {
          "services": {
            "type": "array",
            "description": "As frentes, na ordem do site.",
            "items": {
              "$ref": "#/components/schemas/ServiceSummary"
            }
          }
        }
      },
      "Service": {
        "description": "Uma frente de serviço completa.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ServiceSummary"
          },
          {
            "type": "object",
            "required": [
              "provider",
              "title",
              "description",
              "capabilities",
              "sections",
              "faq",
              "keywords"
            ],
            "properties": {
              "provider": {
                "type": "string",
                "description": "Quem presta o serviço.",
                "examples": [
                  "MFactor"
                ]
              },
              "title": {
                "type": "string",
                "description": "O título da página."
              },
              "description": {
                "type": "string",
                "description": "A descrição curta da página."
              },
              "capabilities": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Os destaques da frente."
              },
              "sections": {
                "type": "array",
                "description": "As seções da página, na ordem.",
                "items": {
                  "$ref": "#/components/schemas/ServiceSection"
                }
              },
              "faq": {
                "type": "array",
                "description": "As perguntas frequentes da frente.",
                "items": {
                  "$ref": "#/components/schemas/FaqEntry"
                }
              },
              "keywords": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Termos pelos quais a frente é buscada."
              }
            }
          }
        ]
      },
      "ServiceSection": {
        "type": "object",
        "description": "Uma seção de página.",
        "required": [
          "heading",
          "paragraphs",
          "items"
        ],
        "properties": {
          "heading": {
            "type": "string",
            "description": "O título da seção."
          },
          "paragraphs": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Os parágrafos corridos."
          },
          "items": {
            "type": "array",
            "description": "Os itens em lista.",
            "items": {
              "type": "object",
              "required": [
                "label",
                "text"
              ],
              "properties": {
                "label": {
                  "type": "string",
                  "description": "O rótulo do item."
                },
                "text": {
                  "type": "string",
                  "description": "O texto do item."
                },
                "url": {
                  "type": "string",
                  "format": "uri",
                  "description": "O link do item, quando houver."
                }
              }
            }
          }
        }
      },
      "FaqEntry": {
        "type": "object",
        "description": "Uma pergunta frequente.",
        "required": [
          "question",
          "answer"
        ],
        "properties": {
          "question": {
            "type": "string",
            "description": "A pergunta."
          },
          "answer": {
            "type": "string",
            "description": "A resposta."
          }
        }
      },
      "Page": {
        "type": "object",
        "description": "Uma página pública do site.",
        "required": [
          "path",
          "kind",
          "title",
          "description",
          "url",
          "markdownUrl",
          "aliases"
        ],
        "properties": {
          "path": {
            "type": "string",
            "description": "O caminho canônico.",
            "examples": [
              "/servicos/mlabs"
            ]
          },
          "kind": {
            "type": "string",
            "enum": [
              "home",
              "service",
              "page"
            ],
            "description": "O tipo da página."
          },
          "title": {
            "type": "string",
            "description": "O título da página."
          },
          "description": {
            "type": "string",
            "description": "A descrição curta da página."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "A URL canônica."
          },
          "markdownUrl": {
            "type": "string",
            "format": "uri",
            "description": "A mesma página em Markdown."
          },
          "aliases": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            },
            "description": "Outras URLs que servem a mesma página."
          }
        }
      },
      "PageList": {
        "type": "object",
        "description": "As páginas públicas.",
        "required": [
          "pages"
        ],
        "properties": {
          "pages": {
            "type": "array",
            "description": "As páginas, na ordem do site.",
            "items": {
              "$ref": "#/components/schemas/Page"
            }
          }
        }
      },
      "ContactRequest": {
        "type": "object",
        "description": "Um pedido de contato. Os limites são os mesmos do formulário do site.",
        "required": [
          "nome",
          "email",
          "mensagem"
        ],
        "properties": {
          "nome": {
            "type": "string",
            "minLength": 2,
            "maxLength": 120,
            "description": "Nome de quem pede o contato."
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 160,
            "description": "E-mail para a resposta."
          },
          "telefone": {
            "type": "string",
            "description": "Telefone brasileiro com DDD, com ou sem +55. Opcional.",
            "examples": [
              "(19) 97148-5856",
              "+55 19 97148-5856"
            ]
          },
          "whatsapp": {
            "type": "boolean",
            "default": true,
            "description": "Se o telefone informado atende no WhatsApp."
          },
          "mensagem": {
            "type": "string",
            "minLength": 10,
            "maxLength": 5000,
            "description": "O que a pessoa precisa: o problema da operação, os sistemas que já existem, o prazo e quem decide."
          }
        }
      },
      "ContactSuccess": {
        "type": "object",
        "description": "Confirmação de envio.",
        "required": [
          "success",
          "message"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Sempre true numa resposta 200."
          },
          "message": {
            "type": "string",
            "description": "Confirmação legível.",
            "examples": [
              "Mensagem enviada."
            ]
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "Um erro no formato Problem Details (RFC 9457), com as extensões code, hint, docs e error.",
        "required": [
          "type",
          "title",
          "status",
          "detail",
          "code",
          "hint",
          "docs",
          "error"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "Identifica o tipo do problema e abre a documentação dos erros."
          },
          "title": {
            "type": "string",
            "description": "O nome do status HTTP."
          },
          "status": {
            "type": "integer",
            "minimum": 400,
            "maximum": 599,
            "description": "O mesmo status HTTP da resposta."
          },
          "detail": {
            "type": "string",
            "description": "O que deu errado, para mostrar a uma pessoa."
          },
          "instance": {
            "type": "string",
            "description": "O caminho que produziu o erro."
          },
          "code": {
            "type": "string",
            "description": "Identificador estável do erro, em snake_case. Os da API toda estão em ErrorCode."
          },
          "hint": {
            "type": "string",
            "description": "O que mudar para a próxima tentativa dar certo."
          },
          "field": {
            "type": "string",
            "description": "O campo do corpo que causou o erro, quando houver."
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "Onde o contrato da API está descrito."
          },
          "error": {
            "type": "string",
            "description": "Cópia de detail, mantida para clientes antigos."
          }
        }
      },
      "ApiProblem": {
        "description": "Um erro comum a todos os endpoints.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Problem"
          },
          {
            "type": "object",
            "properties": {
              "code": {
                "$ref": "#/components/schemas/ErrorCode"
              }
            }
          }
        ]
      },
      "ContactProblem": {
        "description": "Um erro de POST /api/v1/contato.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Problem"
          },
          {
            "type": "object",
            "properties": {
              "code": {
                "$ref": "#/components/schemas/ContactErrorCode"
              },
              "field": {
                "type": "string",
                "enum": [
                  "nome",
                  "email",
                  "telefone",
                  "mensagem"
                ],
                "description": "O campo que causou o erro."
              }
            }
          }
        ]
      },
      "ErrorCode": {
        "type": "string",
        "description": "Os códigos de erro comuns a todos os endpoints.\n\n- `not_found`: o caminho não existe na API.\n- `service_not_found`: o slug de serviço não existe.\n- `method_not_allowed`: método não aceito; veja o cabeçalho Allow.\n- `rate_limited`: limite de taxa estourado; espere o Retry-After.",
        "enum": [
          "not_found",
          "service_not_found",
          "method_not_allowed",
          "rate_limited"
        ]
      },
      "ContactErrorCode": {
        "type": "string",
        "description": "Os códigos de erro de POST /api/v1/contato.\n\n- `invalid_json`: o corpo não é um objeto JSON.\n- `name_required`, `name_too_short`, `name_too_long`: o campo nome.\n- `email_required`, `email_invalid`: o campo email.\n- `phone_invalid`: o telefone não é brasileiro com DDD.\n- `message_required`, `message_too_short`, `message_too_long`: o campo mensagem.\n- `method_not_allowed`: método diferente de POST.\n- `rate_limited`: mais de 10 envios em 10 minutos; espere o Retry-After.\n- `email_delivery_failed`: o servidor de e-mail recusou; tente de novo.\n- `email_not_configured`: falha de configuração do servidor.",
        "enum": [
          "invalid_json",
          "name_required",
          "name_too_short",
          "name_too_long",
          "email_required",
          "email_invalid",
          "phone_invalid",
          "message_required",
          "message_too_short",
          "message_too_long",
          "method_not_allowed",
          "rate_limited",
          "email_delivery_failed",
          "email_not_configured"
        ]
      }
    }
  }
}
