openapi: 3.1.0
info:
  title: TBA Origem · API de leads
  version: "1.0.0"
  summary: Envia leads de outro sistema (loja virtual, ERP, landing com formulário próprio) para o TBA Origem.
  description: |
    Cada envio aceito vira um contato (deduplicado por e-mail ou telefone), um negócio na etapa de entrada do
    funil (ou atualiza o negócio aberto do contato) e a tarefa "Responder lead novo" no Hoje. Todo envio fica
    registrado de forma imutável (LeadSubmission), com origem, UTMs e consentimento.

    - **Autenticação:** chave de API da organização no cabeçalho `Authorization: Bearer tbo_...`. A chave é criada
      pelo dono em Canais › API, aparece uma única vez e é guardada só como hash. Use a chave **apenas no servidor**.
    - **Idempotência:** envie `Idempotency-Key` (até 120 caracteres; o excedente é cortado) para que repetições do
      mesmo envio não dupliquem o lead. A chave vale por chave de API, sem prazo de expiração.
    - **Limites:** 120 envios por minuto por chave de API (janela fixa de 60 s) e corpo de até 50 KB.
    - **Plano:** a captura nunca é bloqueada por plano ou situação da conta (inclusive em somente leitura).
    - **Respostas:** sempre JSON, com `ok` e, em erro, `erro` (mensagem em português) e, quando houver, `campos`
      (mensagem por campo). Cabeçalho `Cache-Control: no-store`.

    Guia com exemplos: https://origem.tbahouse.com.br/ajuda/api
  contact:
    name: TBA House
    url: https://tbahouse.com.br
servers:
  - url: https://origem.tbahouse.com.br
    description: Produção
security:
  - chaveDeApi: []
tags:
  - name: Leads
    description: Entrada de leads servidor a servidor.
paths:
  /api/public/leads:
    post:
      tags: [Leads]
      operationId: enviarLead
      summary: Envia um lead
      description: |
        Obrigatórios: `name` e pelo menos um entre `email` e `phone`. O telefone é normalizado para E.164
        (DDD obrigatório para números do Brasil) e o e-mail é validado; valores inválidos voltam em `campos`.

        LGPD: com `consent.granted = true`, o contato novo entra com base legal **consentimento** e o texto exibido
        é gravado (ConsentRecord). Sem `consent`, vale `legalBasis`; sem nenhum dos dois, **procedimentos
        preliminares**.

        Origem do contato novo: `origem`, se informada; senão deduzida de `utm.source` (instagram, whatsapp,
        indicação); senão "outro". UTMs também são lidas de `pageUrl`, quando presentes na URL.
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: "Identificador único do envio no seu sistema (ex.: número do pedido). Repetir devolve o mesmo resultado com `repetido = true`."
          schema:
            type: string
            maxLength: 120
          example: pedido-1234
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LeadInput"
            examples:
              comConsentimento:
                summary: Lead de landing com consentimento
                value:
                  name: Maria Souza
                  email: maria@exemplo.com.br
                  phone: "(35) 99999-0000"
                  company: Empório Exemplo
                  message: Quero receber a lista de itens disponíveis.
                  origem: site
                  utm: { source: google, medium: cpc, campaign: safra }
                  pageUrl: https://exemplo.com.br/contato
                  consent: { granted: true, text: Autorizo o contato comercial da Empório Exemplo. }
              semConsentimento:
                summary: Cliente de loja virtual (execução de contrato)
                value:
                  name: João Lima
                  phone: "+55 11 98888-7777"
                  message: Pedido 1234 na loja virtual
                  legalBasis: execucao_contrato
      responses:
        "201":
          description: Lead novo recebido (contato e/ou negócio criados).
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LeadResult"
              example:
                ok: true
                id: cm4x0abc0000001
                contatoId: cm4x0abc0000002
                negocioId: cm4x0abc0000003
                contatoNovo: true
                negocioNovo: true
                repetido: false
        "200":
          description: |
            Recebido sem criar lead novo: o contato já existia e tinha negócio aberto (o envio entra na ficha e gera a
            tarefa "Lead voltou a fazer contato"), ou o `Idempotency-Key` já tinha sido recebido (`repetido = true`).
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LeadResult"
              examples:
                repetido:
                  value:
                    ok: true
                    id: cm4x0abc0000001
                    contatoId: cm4x0abc0000002
                    negocioId: cm4x0abc0000003
                    contatoNovo: false
                    negocioNovo: false
                    repetido: true
        "400":
          description: Corpo que não é JSON válido ou maior que 50 KB.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Erro"
              example:
                ok: false
                erro: Corpo JSON inválido ou maior que 50 KB.
        "401":
          description: "Sem `Authorization: Bearer`, chave que não começa com `tbo_`, chave desconhecida ou revogada."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Erro"
              example:
                ok: false
                erro: Chave de API ausente, inválida ou revogada.
        "422":
          description: |
            Dados que não passam na validação. Três formas:
            - `erro = "Campos inválidos."`: tipo errado ou tamanho acima do limite; `campos` usa o caminho do campo
              (ex.: `consent.text`).
            - `erro = "legalBasis inválida."`: `campos.legalBasis` lista os valores aceitos.
            - `erro = "Falta alguma informação ou há algo para corrigir nos campos marcados."`: falta o nome, falta
              e-mail e telefone, ou e-mail/telefone incompletos; `campos` traz `name`, `email` ou `phone`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Erro"
              examples:
                camposObrigatorios:
                  value:
                    ok: false
                    erro: Falta alguma informação ou há algo para corrigir nos campos marcados.
                    campos:
                      phone: "Confira o telefone, com DDD. Ex.: (35) 99999-0000"
                semContato:
                  value:
                    ok: false
                    erro: Falta alguma informação ou há algo para corrigir nos campos marcados.
                    campos:
                      email: Informe e-mail ou telefone.
                tipoErrado:
                  value:
                    ok: false
                    erro: Campos inválidos.
                    campos:
                      consent.text: "Too small: expected string to have >=5 characters"
                baseLegal:
                  value:
                    ok: false
                    erro: legalBasis inválida.
                    campos:
                      legalBasis: consentimento, legitimo_interesse, execucao_contrato, procedimentos_preliminares
        "429":
          description: Mais de 120 envios no mesmo minuto com a mesma chave. Tente de novo no minuto seguinte.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Erro"
              example:
                ok: false
                erro: Limite de 120 envios por minuto atingido.
        "500":
          description: Falha inesperada (o envio não foi gravado). Pode repetir com o mesmo `Idempotency-Key`.
components:
  securitySchemes:
    chaveDeApi:
      type: http
      scheme: bearer
      bearerFormat: tbo_<chave>
      description: Chave de API criada em Canais › API (começa com `tbo_`).
  headers:
    NoStore:
      description: As respostas nunca são guardadas em cache.
      schema:
        type: string
        const: no-store
  schemas:
    LeadInput:
      type: object
      required: [name]
      description: Precisa de `email` ou `phone` (pelo menos um). Campos desconhecidos são ignorados (o corpo bruto é guardado por 12 meses).
      properties:
        name:
          type: string
          maxLength: 160
          description: Nome da pessoa. Não pode ficar vazio.
          example: Maria Souza
        email:
          type: [string, "null"]
          maxLength: 200
          example: maria@exemplo.com.br
        phone:
          type: [string, "null"]
          maxLength: 40
          description: Telefone com DDD (formatos livres; DDI opcional, padrão Brasil). Guardado em E.164.
          example: "(35) 99999-0000"
        company:
          type: [string, "null"]
          maxLength: 160
          description: Empresa do lead. Criada (ou reaproveitada pelo nome) e ligada ao contato.
        jobTitle:
          type: [string, "null"]
          maxLength: 120
          description: Cargo.
        message:
          type: [string, "null"]
          maxLength: 5000
          description: Mensagem do lead. Vira uma nota no contato e no negócio.
        origem:
          type: [string, "null"]
          maxLength: 40
          description: |
            Origem registrada no contato novo. Valores reconhecidos (sem diferenciar maiúsculas): `site`,
            `instagram`, `whatsapp`, `indicacao` (ou `indicação`), `outro`. Outros valores são ignorados.
          examples: [site]
        utm:
          type: [object, "null"]
          properties:
            source: { type: [string, "null"], maxLength: 200 }
            medium: { type: [string, "null"], maxLength: 200 }
            campaign: { type: [string, "null"], maxLength: 200 }
            term: { type: [string, "null"], maxLength: 200 }
            content: { type: [string, "null"], maxLength: 200 }
        pageUrl:
          type: [string, "null"]
          maxLength: 2000
          description: Página onde o lead foi capturado (só http/https; URLs inválidas são descartadas).
        referrer:
          type: [string, "null"]
          maxLength: 2000
        consent:
          type: [object, "null"]
          required: [granted, text]
          description: Consentimento coletado no seu formulário, com o texto exato exibido à pessoa.
          properties:
            granted:
              type: boolean
            text:
              type: string
              minLength: 5
              maxLength: 1000
        legalBasis:
          type: [string, "null"]
          description: Base legal quando não há `consent` (sem diferenciar maiúsculas).
          enum: [consentimento, legitimo_interesse, execucao_contrato, procedimentos_preliminares, null]
    LeadResult:
      type: object
      required: [ok, id, contatoId, negocioId, contatoNovo, negocioNovo, repetido]
      properties:
        ok:
          type: boolean
          const: true
        id:
          type: string
          description: Id do envio (LeadSubmission).
        contatoId:
          type: string
        negocioId:
          type: [string, "null"]
          description: Negócio criado ou reaproveitado. `null` só se a organização não tiver nenhum funil.
        contatoNovo:
          type: boolean
        negocioNovo:
          type: boolean
        repetido:
          type: boolean
          description: "`true` quando o `Idempotency-Key` já tinha sido recebido (nada novo foi gravado)."
    Erro:
      type: object
      required: [ok, erro]
      properties:
        ok:
          type: boolean
          const: false
        erro:
          type: string
          description: O que aconteceu, em português.
        campos:
          type: object
          additionalProperties: { type: string }
          description: Mensagem por campo (chave = nome ou caminho do campo).
