{
  "openapi": "3.1.0",
  "info": {
    "title": "Speakus — API de eventos",
    "version": "1.0.0",
    "summary": "Webhook de entrada: o SaaS do cliente avisa o Speakus sobre eventos do ciclo de vida do usuário e sobre a conversão.",
    "description": "O mesmo endpoint recebe os dois momentos da integração:\n\n1. **Evento de ciclo de vida** (`trial_ended`, `checkout_abandonado`, `cadastro_sem_ativacao`, ...): o Speakus cria/atualiza o lead e prepara a abordagem no WhatsApp.\n2. **Aviso de conversão** (`event: \"converted\"`): fecha o ciclo — o lead é marcado como recuperado e abordagens pendentes são canceladas. Não dispara mensagem.\n\nAutenticação por assinatura HMAC-SHA256 do corpo cru no header `X-Speakus-Signature`. A URL contém a `ingestKey` do cliente. Ambas as credenciais são emitidas no painel do Speakus em Configurações → Integrações.\n\nOs campos de `data` são **dinâmicos**: qualquer chave escalar válida é aceita e vira atributo do lead, disponível como contexto para a IA na conversa.",
    "contact": { "name": "Speakus", "url": "https://speakus.app" }
  },
  "externalDocs": { "description": "Documentação legível", "url": "https://speakus.app/docs/api" },
  "servers": [{ "url": "https://app.speakus.app", "description": "Produção" }],
  "security": [{ "hmacSignature": [] }],
  "paths": {
    "/v1/hooks/inbound/{ingestKey}": {
      "post": {
        "operationId": "ingestEvent",
        "summary": "Envia um evento do SaaS para o Speakus",
        "description": "Idempotente por `eventId`: reenviar o mesmo `eventId` devolve a mesma resposta sem duplicar lead nem abordagem. Em erro 5xx, o retry com o mesmo `eventId` é seguro.",
        "parameters": [
          {
            "name": "ingestKey",
            "in": "path",
            "required": true,
            "description": "Chave de ingestão do workspace, emitida no painel (formato `wk_…`).",
            "schema": { "type": "string", "pattern": "^wk_[0-9a-f]{36}$" }
          },
          {
            "name": "X-Speakus-Signature",
            "in": "header",
            "required": true,
            "description": "`sha256=` + HMAC-SHA256 hexadecimal do corpo cru da requisição, usando o segredo do webhook.",
            "schema": { "type": "string", "pattern": "^sha256=[0-9a-f]{64}$" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/InboundEvent" },
              "examples": {
                "fimDeTrial": {
                  "summary": "Trial acabou sem conversão",
                  "value": {
                    "eventId": "trial_usr_412",
                    "event": "trial_ended",
                    "occurredAt": "2026-07-27T18:00:00Z",
                    "lead": { "phone": "+5585999998888", "name": "Marina Silva", "email": "marina@empresa.com", "externalId": "usr_412" },
                    "data": { "plan": "pro", "trial_days": 14, "last_feature_used": "relatorios", "login_count": 7 }
                  }
                },
                "conversao": {
                  "summary": "O usuário voltou e assinou",
                  "value": {
                    "eventId": "conv_usr_412",
                    "event": "converted",
                    "occurredAt": "2026-07-29T14:30:00Z",
                    "lead": { "phone": "+5585999998888" },
                    "data": { "plan_purchased": "pro", "mrr": 149 }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Evento aceito e processado.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/IngestAccepted" },
                "example": { "received": true, "eventId": "trial_usr_412", "leadId": "cms3td20u000usilbkhg8ghtu", "scheduledMessageId": "cms3tpwpe0005bylbu9sijipl", "warnings": [] }
              }
            }
          },
          "400": { "description": "`eventId` ausente, JSON inválido ou corpo maior que 64 KB.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Assinatura HMAC ausente ou inválida.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Webhook pausado no painel.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "`ingestKey` desconhecida.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Evento fora do catálogo do workspace ou telefone inválido (esperado E.164).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "503": { "description": "Ingestão temporariamente desabilitada.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "hmacSignature": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Speakus-Signature",
        "description": "HMAC-SHA256 do corpo cru: `sha256=<hex>`. Assine exatamente a string enviada no corpo."
      }
    },
    "schemas": {
      "InboundEvent": {
        "type": "object",
        "required": ["eventId", "event", "lead"],
        "properties": {
          "eventId": {
            "type": "string",
            "maxLength": 120,
            "description": "Identificador único e determinístico do acontecimento (ex.: `trial_<userId>`). Base da idempotência."
          },
          "event": {
            "type": "string",
            "description": "Chave do evento. Precisa estar mapeada nos Gatilhos de um Funcionário no painel — exceto `converted`, que é reservada para a confirmação de conversão.",
            "examples": ["trial_ended", "checkout_abandonado", "cadastro_sem_ativacao", "assinatura_cancelada", "uso_despencou", "converted"]
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time",
            "description": "Quando o fato aconteceu no seu sistema (ISO 8601). Usado nas métricas; default é o horário de recebimento."
          },
          "lead": { "$ref": "#/components/schemas/Lead" },
          "data": {
            "type": "object",
            "description": "Dados dinâmicos do usuário. Qualquer chave escalar válida é aceita (máx. 30 por evento) e vira atributo do lead. Chaves em snake_case iniciando por letra; valores string, número, booleano ou data ISO, até 2000 caracteres. Objetos aninhados são ignorados.",
            "additionalProperties": { "type": ["string", "number", "boolean"] },
            "examples": [{ "plan": "pro", "trial_days": 14, "stuck_at": "nao_importou_dados", "is_team_admin": true }]
          },
          "schedule": {
            "type": "object",
            "description": "Adia a abordagem. Sem isto, ela é preparada imediatamente.",
            "properties": {
              "delay": { "type": "integer", "minimum": 0, "description": "Segundos a esperar (máx. 30 dias)." },
              "sendAt": { "type": "string", "format": "date-time", "description": "Momento exato do disparo (ISO 8601)." }
            }
          }
        }
      },
      "Lead": {
        "type": "object",
        "required": ["phone"],
        "description": "Identificação do usuário. O telefone é a âncora: é por ele que o Speakus reconhece a mesma pessoa entre eventos.",
        "properties": {
          "phone": { "type": "string", "pattern": "^\\+[1-9][0-9]{7,14}$", "description": "Telefone em E.164 (obrigatório).", "examples": ["+5585999998888"] },
          "name": { "type": "string", "maxLength": 80, "description": "Nome. Só preenche a ficha se ela estiver vazia — nunca sobrescreve." },
          "email": { "type": "string", "format": "email", "maxLength": 160 },
          "externalId": { "type": "string", "maxLength": 120, "description": "O id do usuário no seu sistema. Liga os dois lados." }
        }
      },
      "IngestAccepted": {
        "type": "object",
        "required": ["received", "eventId"],
        "properties": {
          "received": { "type": "boolean" },
          "eventId": { "type": "string" },
          "leadId": { "type": ["string", "null"], "description": "Id do lead no Speakus." },
          "scheduledMessageId": { "type": ["string", "null"], "description": "Id da abordagem criada, quando houver Funcionário ativo mapeado para o evento." },
          "warnings": { "type": "array", "items": { "type": "string" }, "description": "Avisos não bloqueantes: chaves ignoradas, evento sem funcionário vinculado, valor preservado por já ter sido coletado em conversa." },
          "replayed": { "type": "boolean", "description": "true quando o `eventId` já havia sido processado." }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string", "description": "Motivo legível da recusa." },
          "received": { "type": "boolean" },
          "eventId": { "type": "string" },
          "warnings": { "type": "array", "items": { "type": "string" } }
        }
      }
    }
  }
}
