{
  "openapi": "3.1.0",
  "info": {
    "title": "Tellia API",
    "version": "1.0.0",
    "summary": "Análise de comunicação por vídeo para integrar no seu produto.",
    "description": "Envie um vídeo e receba nota de 0 a 100 na régua universal, dimensões, régua de cenário com critérios de 0 a 4, métricas da fala e relatório. Chave de teste (tl_test_) = sandbox: respostas simuladas, custo zero, nada vai para a IA. Chave de produção (tl_live_) = análise real, descontada do saldo pré-pago em reais. Valores de dinheiro sempre em centavos de real. Há um webhook de teste e um de produção, cada um com URL e segredo próprios (painel, Minha empresa > API); os eventos de cada ambiente vão só para o webhook dele. Resultados, de produção e do sandbox, ficam guardados por até 12 meses e então são apagados automaticamente. Método HTTP errado numa rota que existe responde 405 (`metodo_invalido`) com o cabeçalho `Allow`; caminho que não existe, inclusive a URL base /v1 sozinha, responde 404 (`nao_encontrada`), também em JSON. Guia completo: https://www.tellia.com.br/desenvolvedores/docs",
    "termsOfService": "https://www.tellia.com.br/termos/api",
    "contact": { "name": "Tellia", "url": "https://www.tellia.com.br/desenvolvedores", "email": "contato@tellia.com.br" }
  },
  "servers": [{ "url": "https://www.tellia.com.br/api", "description": "Produção e sandbox (o ambiente é decidido pelo prefixo da chave)" }],
  "security": [{ "chave": [] }],
  "tags": [
    { "name": "Envio", "description": "Envio do vídeo em partes." },
    { "name": "Análises", "description": "Criar, consultar, listar e apagar análises." },
    { "name": "Catálogo", "description": "Réguas disponíveis." },
    { "name": "Conta", "description": "Uso do dia, saldo e limites." }
  ],
  "paths": {
    "/v1/uploads": {
      "post": {
        "tags": ["Envio"],
        "operationId": "criarUpload",
        "summary": "Abrir um envio de vídeo",
        "description": "Devolve a URL e os cabeçalhos para mandar o vídeo em partes. Protocolo: corte o arquivo em partes de `parteBytes` (múltiplo de 256 KiB; só a última pode ser menor) e mande em ordem, cada uma com POST para `envio.url`, os cabeçalhos de `envio.cabecalhos` e mais `x-offset` (byte inicial da parte). Na última parte, inclua `x-final: 1`; a resposta dela traz `{ \"file\": { \"name\": \"files/...\" } }`, e esse `name` é o `fileName` da criação. Com chave de teste, `simulado` é true e `envio` é null (não há o que enviar). Em produção, abrir um envio exige saldo para ao menos uma análise (senão, 402 `saldo_insuficiente`); nada é descontado aqui, o valor sai na criação. Também em produção, a empresa pode ter no máximo 5 envios abertos sem análise nas últimas 48 horas, e nunca mais do que o saldo paga (saldo dividido pelo preço da análise); acima disso, 429 `limite_requisicoes`. Use um envio para cada vídeo. Vídeo acima de 1 GB cuja duração não pôde ser medida é recusado: na criação, com 400 `upload_invalido` (nada é descontado), ou, se ainda estava em processamento na criação, na fila, e então a análise termina em `falhou` e o valor volta ao saldo.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NovoUpload" } } } },
        "responses": {
          "201": { "description": "Envio aberto.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Upload" } } } },
          "400": { "$ref": "#/components/responses/PedidoInvalido" },
          "401": { "$ref": "#/components/responses/NaoAutenticado" },
          "402": { "$ref": "#/components/responses/SaldoInsuficiente" },
          "403": { "$ref": "#/components/responses/Proibido" },
          "405": { "$ref": "#/components/responses/MetodoInvalido" },
          "429": { "$ref": "#/components/responses/Limite" },
          "500": { "$ref": "#/components/responses/ErroInterno" }
        }
      }
    },
    "/v1/analises": {
      "post": {
        "tags": ["Análises"],
        "operationId": "criarAnalise",
        "summary": "Criar uma análise",
        "description": "Produção: responde 202 com `status: na_fila` e avisa pelo webhook quando terminar. Sandbox: responde 201 com `status: pronta` e `simulado: true`, e dispara o webhook `analise.concluida`. A mesma `Idempotency-Key`, pela mesma empresa, devolve a mesma análise no estado atual, com o mesmo status HTTP e o cabeçalho `Idempotent-Replayed: true`, sem cobrar de novo e sem disparar outro webhook. A mesma chave usada com a chave do outro ambiente, ou depois de a análise ser apagada, responde 409.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Valor único por análise (por exemplo, um UUID), de 1 a 255 caracteres visíveis, sem espaço. Reaproveite ao repetir a mesma criação.",
            "schema": { "type": "string", "minLength": 1, "maxLength": 255, "pattern": "^[!-~]{1,255}$" }
          }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NovaAnalise" } } } },
        "responses": {
          "201": {
            "description": "Sandbox: análise simulada, já pronta.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResultadoApi" } } },
            "headers": { "Idempotent-Replayed": { "$ref": "#/components/headers/IdempotentReplayed" } }
          },
          "202": {
            "description": "Produção: análise criada e na fila.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResultadoApi" } } },
            "headers": { "Idempotent-Replayed": { "$ref": "#/components/headers/IdempotentReplayed" } }
          },
          "400": { "$ref": "#/components/responses/PedidoInvalido" },
          "401": { "$ref": "#/components/responses/NaoAutenticado" },
          "402": { "$ref": "#/components/responses/SaldoInsuficiente" },
          "403": { "$ref": "#/components/responses/Proibido" },
          "405": { "$ref": "#/components/responses/MetodoInvalido" },
          "409": { "$ref": "#/components/responses/ConflitoIdempotencia" },
          "429": { "$ref": "#/components/responses/Limite" },
          "500": { "$ref": "#/components/responses/ErroInterno" }
        }
      },
      "get": {
        "tags": ["Análises"],
        "operationId": "listarAnalises",
        "summary": "Listar análises",
        "description": "Das mais novas para as mais antigas, só do ambiente da chave usada. Enquanto `temMais` for true, passe `proximo` como `antesDe`.",
        "parameters": [
          { "name": "limite", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 } },
          {
            "name": "antesDe",
            "in": "query",
            "required": false,
            "description": "Só análises criadas antes desta data e hora, com fuso explícito (a mesma regra de `consentimento.coletadoEm`). Use o `proximo` da página anterior; um fuso com + vai na URL como %2B.",
            "schema": { "type": "string", "format": "date-time", "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])[Tt]([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?([Zz]|[+-]([01]\\d|2[0-3]):[0-5]\\d)$" }
          }
        ],
        "responses": {
          "200": {
            "description": "Uma página de análises.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ListaAnalises" } } }
          },
          "400": { "$ref": "#/components/responses/PedidoInvalido" },
          "401": { "$ref": "#/components/responses/NaoAutenticado" },
          "403": { "$ref": "#/components/responses/Proibido" },
          "405": { "$ref": "#/components/responses/MetodoInvalido" },
          "429": { "$ref": "#/components/responses/Limite" },
          "500": { "$ref": "#/components/responses/ErroInterno" }
        }
      }
    },
    "/v1/analises/{id}": {
      "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
      "get": {
        "tags": ["Análises"],
        "operationId": "lerAnalise",
        "summary": "Consultar uma análise",
        "description": "O mesmo objeto que chega no webhook. Análise apagada (por você ou pela guarda de até 12 meses), do outro ambiente ou de outra empresa responde 404 (a existência não é confirmada).",
        "responses": {
          "200": {
            "description": "Resultado da análise.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResultadoApi" } } }
          },
          "401": { "$ref": "#/components/responses/NaoAutenticado" },
          "403": { "$ref": "#/components/responses/Proibido" },
          "404": { "$ref": "#/components/responses/NaoEncontrada" },
          "405": { "$ref": "#/components/responses/MetodoInvalido" },
          "429": { "$ref": "#/components/responses/Limite" },
          "500": { "$ref": "#/components/responses/ErroInterno" }
        }
      },
      "delete": {
        "tags": ["Análises"],
        "operationId": "apagarAnalise",
        "summary": "Apagar uma análise (LGPD)",
        "description": "Apaga o resultado de vez. Responde 409 (`conflito`) enquanto a análise está `na_fila` ou `processando`. Apagar não devolve o valor da análise. Sem isso, o resultado (de produção ou do sandbox) é apagado automaticamente depois de 12 meses.",
        "responses": {
          "200": {
            "description": "Análise apagada.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AnaliseApagada" } } }
          },
          "401": { "$ref": "#/components/responses/NaoAutenticado" },
          "403": { "$ref": "#/components/responses/Proibido" },
          "404": { "$ref": "#/components/responses/NaoEncontrada" },
          "405": { "$ref": "#/components/responses/MetodoInvalido" },
          "409": { "$ref": "#/components/responses/Conflito" },
          "429": { "$ref": "#/components/responses/Limite" },
          "500": { "$ref": "#/components/responses/ErroInterno" }
        }
      }
    },
    "/v1/reguas": {
      "get": {
        "tags": ["Catálogo"],
        "operationId": "listarReguas",
        "summary": "Listar réguas",
        "description": "A régua universal (em toda análise) e as réguas de cenário que podem ir em `regua` na criação.",
        "responses": {
          "200": {
            "description": "Catálogo de réguas.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ListaReguas" } } }
          },
          "401": { "$ref": "#/components/responses/NaoAutenticado" },
          "403": { "$ref": "#/components/responses/Proibido" },
          "405": { "$ref": "#/components/responses/MetodoInvalido" },
          "429": { "$ref": "#/components/responses/Limite" },
          "500": { "$ref": "#/components/responses/ErroInterno" }
        }
      }
    },
    "/v1/uso": {
      "get": {
        "tags": ["Conta"],
        "operationId": "lerUso",
        "summary": "Uso, saldo e limites",
        "responses": {
          "200": {
            "description": "Uso da chave hoje e saldo da empresa.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Uso" } } }
          },
          "401": { "$ref": "#/components/responses/NaoAutenticado" },
          "403": { "$ref": "#/components/responses/Proibido" },
          "405": { "$ref": "#/components/responses/MetodoInvalido" },
          "429": { "$ref": "#/components/responses/Limite" },
          "500": { "$ref": "#/components/responses/ErroInterno" }
        }
      }
    }
  },
  "webhooks": {
    "analise.concluida": { "$ref": "#/components/pathItems/EventoAnalise" },
    "analise.falhou": { "$ref": "#/components/pathItems/EventoAnalise" },
    "analise.retida": { "$ref": "#/components/pathItems/EventoAnalise" }
  },
  "components": {
    "securitySchemes": {
      "chave": {
        "type": "http",
        "scheme": "bearer",
        "description": "Chave da API no cabeçalho `Authorization: Bearer tl_test_...` (sandbox) ou `Authorization: Bearer tl_live_...` (produção). O nome do esquema não diferencia maiúsculas de minúsculas (`Bearer`, `bearer` e `BEARER` valem). Sem chave válida, a resposta é 401 com o cabeçalho `WWW-Authenticate: Bearer`. A chave aparece uma única vez, na criação, e nunca deve ir para o navegador ou o app do usuário final."
      }
    },
    "pathItems": {
      "EventoAnalise": {
        "post": {
          "summary": "Evento de análise",
          "description": "A Tellia faz POST na URL do webhook do ambiente da análise. São dois webhooks, configurados no painel (Minha empresa > API), cada um com URL e segredo próprios: eventos de análises da chave de teste vão só para o webhook de teste, e os da produção, só para o de produção. A URL precisa ser HTTPS, num domínio público com ponto, na porta 443 ou sem porta, sem usuário e senha (a Tellia se identifica pela assinatura `Tellia-Assinatura`) e sem espaço ou caractere de controle; IP e endereço local não são aceitos, e um destino que resolve para rede interna é recusado na hora do envio. A URL é gravada na forma normalizada (por exemplo, sem `:443` e com o domínio em minúsculas). Assinatura no cabeçalho `Tellia-Assinatura: t=<unix em segundos>,v1=<hex>`, onde v1 = HMAC-SHA256 de `${t}.${corpo}` com o segredo `whsec_...` do webhook daquele ambiente como chave e o corpo bruto recebido. Recuse se `t` estiver a mais de 300 segundos do seu relógio. Responda 2xx em até 10 s; sem isso, há retentativas em 1 min, 5 min, 30 min, 2 h e 6 h (até 6 tentativas), sempre com o mesmo `id` de evento. As esperas são o mínimo: as retentativas de cada empresa saem em pequenos lotes, em paralelo, então um evento pode chegar um pouco depois da espera, junto com outros e fora de ordem (decida por `dados.status`). Redirecionamento não é seguido.",
          "parameters": [
            {
              "name": "Tellia-Assinatura",
              "in": "header",
              "required": true,
              "schema": { "type": "string", "pattern": "^t=\\d+,v1=[0-9a-f]{64}$" },
              "example": "t=1793368920,v1=56844b353e19ef55ee6c3fbce1cb369f679b4c4e8bd61f7ae50892986a3518a5"
            }
          ],
          "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Evento" } } } },
          "responses": { "2XX": { "description": "Recebido. Qualquer 2xx encerra as retentativas." } }
        }
      }
    },
    "responses": {
      "PedidoInvalido": {
        "description": "Pedido inválido (`pedido_invalido`, com `campo`; inclui campo com o tipo errado, como um número onde se espera texto) ou envio inválido (`upload_invalido`: envio inexistente, vencido, já usado ou de outra empresa; `fileName` que não confere; vídeo ilegível, mais longo que 600 s ou sem a duração medida e com mais de 1 GB).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erro" } } }
      },
      "NaoAutenticado": {
        "description": "Chave ausente, com outro esquema que não Bearer, inválida ou da empresa desativada (`nao_autenticado`), ou revogada (`chave_revogada`).",
        "headers": { "WWW-Authenticate": { "$ref": "#/components/headers/WWWAuthenticate" } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erro" } } }
      },
      "SaldoInsuficiente": {
        "description": "Saldo insuficiente ou vencido (`saldo_insuficiente`). Traz `saldoCentavos` e `precoCentavos`.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erro" } } }
      },
      "Proibido": {
        "description": "Termos da API pendentes (`termos_pendentes`) ou produção ainda não liberada (`producao_desligada`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erro" } } }
      },
      "NaoEncontrada": {
        "description": "Análise inexistente, apagada, do outro ambiente ou de outra empresa (`nao_encontrada`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erro" } } }
      },
      "Conflito": {
        "description": "A análise ainda está na fila ou em processamento (`conflito`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erro" } } }
      },
      "Limite": {
        "description": "Limite de requisições por minuto (`limite_requisicoes`, também numa rajada de chamadas ao mesmo tempo; em POST /v1/uploads, também envios demais sem análise) ou teto de gasto do dia da chave (`teto_diario`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erro" } } }
      },
      "MetodoInvalido": {
        "description": "Método HTTP que a rota não aceita (`metodo_invalido`). O cabeçalho `Allow` lista os métodos aceitos.",
        "headers": { "Allow": { "$ref": "#/components/headers/Allow" } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erro" } } }
      },
      "ErroInterno": {
        "description": "Falha do lado da Tellia (`erro_interno`). Repita com a mesma Idempotency-Key.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erro" } } }
      },
      "ConflitoIdempotencia": {
        "description": "A `Idempotency-Key` já foi usada com a chave do outro ambiente, ou a análise que ela criou foi apagada (`conflito`). Use outra.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erro" } } }
      }
    },
    "headers": {
      "IdempotentReplayed": {
        "description": "true quando a resposta é a repetição de uma criação anterior com a mesma `Idempotency-Key`.",
        "schema": { "type": "string", "enum": ["true"] }
      },
      "Allow": {
        "description": "Métodos aceitos pela rota, separados por vírgula (por exemplo, `GET, DELETE`).",
        "schema": { "type": "string" }
      },
      "WWWAuthenticate": {
        "description": "Em todo 401: o esquema de autenticação esperado, Bearer.",
        "schema": { "type": "string", "pattern": "^Bearer" }
      }
    },
    "schemas": {
      "Erro": {
        "type": "object",
        "required": ["erro"],
        "properties": {
          "erro": {
            "type": "object",
            "required": ["codigo", "mensagem"],
            "properties": {
              "codigo": { "$ref": "#/components/schemas/CodigoErro" },
              "mensagem": { "type": "string", "description": "Explicação em português, para quem integra." },
              "campo": { "type": "string", "description": "Campo com problema, quando o erro é de um campo." },
              "saldoCentavos": { "type": "integer", "description": "Em `saldo_insuficiente`: saldo disponível." },
              "precoCentavos": { "type": "integer", "description": "Em `saldo_insuficiente`: preço da análise." }
            },
            "additionalProperties": true
          }
        },
        "example": {
          "erro": {
            "codigo": "saldo_insuficiente",
            "mensagem": "Saldo insuficiente. Compre saldo no painel (Minha empresa > API).",
            "saldoCentavos": 1200,
            "precoCentavos": 14900
          }
        }
      },
      "CodigoErro": {
        "type": "string",
        "enum": [
          "nao_autenticado",
          "chave_revogada",
          "termos_pendentes",
          "producao_desligada",
          "limite_requisicoes",
          "teto_diario",
          "saldo_insuficiente",
          "pedido_invalido",
          "upload_invalido",
          "nao_encontrada",
          "metodo_invalido",
          "conflito",
          "erro_interno"
        ]
      },
      "Ambiente": { "type": "string", "enum": ["test", "live"] },
      "Status": {
        "type": "string",
        "enum": ["na_fila", "processando", "pronta", "em_revisao", "nao_entregue", "falhou"],
        "description": "`pronta` é o único estado com nota, métricas e relatório. `em_revisao`: retida para conferência da equipe (o motivo não é informado). `nao_entregue`: retenção confirmada (o valor não volta). `falhou`: se a falha foi da Tellia, o valor volta ao saldo."
      },
      "DimensaoId": { "type": "string", "enum": ["clareza", "dominioAssunto", "seguranca", "engajamento"] },
      "NovoUpload": {
        "type": "object",
        "required": ["mimeType", "tamanhoBytes"],
        "properties": {
          "mimeType": { "type": "string", "enum": ["video/mp4", "video/webm", "video/quicktime", "video/x-matroska"] },
          "tamanhoBytes": { "type": "integer", "minimum": 1, "maximum": 1000000000, "description": "Tamanho exato do arquivo, em bytes (até 1 GB)." }
        }
      },
      "Upload": {
        "type": "object",
        "required": ["objeto", "uploadId", "expiraEm", "simulado", "envio"],
        "properties": {
          "objeto": { "const": "upload" },
          "uploadId": { "type": "string", "pattern": "^up_[A-Za-z0-9]{24}$" },
          "expiraEm": {
            "type": "string",
            "format": "date-time",
            "description": "60 minutos depois da abertura. Envie o vídeo e crie a análise antes disso."
          },
          "simulado": { "type": "boolean", "description": "true com chave de teste." },
          "envio": { "oneOf": [{ "$ref": "#/components/schemas/EnvioUpload" }, { "type": "null" }], "description": "null no sandbox." }
        }
      },
      "EnvioUpload": {
        "type": "object",
        "required": ["url", "metodo", "cabecalhos", "parteBytes", "instrucoes"],
        "properties": {
          "url": { "type": "string", "format": "uri" },
          "metodo": { "const": "POST" },
          "cabecalhos": {
            "type": "object",
            "description": "Mande exatamente como vieram, em todas as partes, junto com `x-offset` (e `x-final: 1` na última).",
            "required": ["x-session-url", "x-exp", "x-sig"],
            "properties": { "x-session-url": { "type": "string" }, "x-exp": { "type": "string" }, "x-sig": { "type": "string" } }
          },
          "parteBytes": {
            "type": "integer",
            "const": 16777216,
            "description": "Tamanho de cada parte (múltiplo de 256 KiB). Só a última pode ser menor."
          },
          "instrucoes": { "type": "string" }
        }
      },
      "NovaAnalise": {
        "type": "object",
        "required": ["duracaoSeg", "consentimento"],
        "properties": {
          "uploadId": {
            "type": "string",
            "pattern": "^up_[A-Za-z0-9]{24}$",
            "description": "Obrigatório em produção. No sandbox, opcional (se mandar, mande também `fileName`)."
          },
          "fileName": {
            "type": "string",
            "pattern": "^files/[A-Za-z0-9-]{1,100}$",
            "description": "O `file.name` devolvido pela última parte do envio. Obrigatório em produção."
          },
          "duracaoSeg": {
            "type": "number",
            "minimum": 0.5,
            "maximum": 600,
            "description": "Duração do vídeo em segundos (até 10 minutos). É arredondada para o segundo mais próximo e precisa dar pelo menos 1 (de 0,5 para cima)."
          },
          "regua": {
            "type": "string",
            "default": "universal",
            "enum": ["universal", "vendas", "lideranca", "conversa-dificil", "atendimento", "imprensa", "ensino", "entrevista-candidato"],
            "description": "Id de GET /v1/reguas. A universal vem sempre; uma régua de cenário acrescenta `cenario` ao resultado."
          },
          "contexto": {
            "type": ["object", "null"],
            "description": "Ausente ou null é o mesmo que não informar. Um valor que não é objeto, ou um campo que não é texto, é recusado com 400 `pedido_invalido` e o `campo`. Texto além do limite é cortado: o limite conta pontos de código Unicode, como o maxLength, e o corte nunca parte um caractere ao meio (um emoji feito de vários pontos de código, como uma bandeira, pode perder a parte final).",
            "properties": {
              "situacao": { "type": ["string", "null"], "maxLength": 120, "description": "O momento da fala. Texto além do limite é cortado. Com régua de cenário, vale a situação da própria régua e este campo não é usado: descreva o caso em `publico` e `objetivo`." },
              "publico": { "type": ["string", "null"], "maxLength": 120, "description": "Para quem a pessoa fala. Texto além do limite é cortado." },
              "objetivo": { "type": ["string", "null"], "maxLength": 300, "description": "O que a fala quer conseguir. Texto além do limite é cortado." }
            }
          },
          "consentimento": {
            "type": "object",
            "required": ["coletadoEm", "versaoTexto"],
            "description": "Atestado do consentimento da pessoa filmada (Termos da API, seção 5).",
            "properties": {
              "coletadoEm": {
                "type": "string",
                "format": "date-time",
                "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])[Tt]([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d{1,9})?)?([Zz]|[+-]([01]\\d|2[0-3]):[0-5]\\d)$",
                "description": "Quando a pessoa consentiu: data e hora com fuso explícito (Z ou ±hh:mm), com ou sem segundos, como 2026-10-30T13:58:00Z ou 2026-10-30T10:58:00-03:00. Sem fuso, só a data ou outro formato é recusado. Não pode estar no futuro: aceitamos até 5 minutos à frente do nosso relógio, pela diferença entre relógios."
              },
              "versaoTexto": {
                "type": "string",
                "minLength": 1,
                "maxLength": 60,
                "description": "Qual texto de consentimento a pessoa aceitou. Acima de 60 caracteres, a criação é recusada."
              }
            }
          },
          "referencia": {
            "type": ["string", "null"],
            "maxLength": 100,
            "description": "Seu id interno, devolvido no resultado. Não use nome nem e-mail. Texto além do limite é cortado (sem partir um caractere ao meio); valor que não é texto, como um número, é recusado com 400 `pedido_invalido`. Ausente ou null: sem referência."
          },
          "produto": { "type": "string", "enum": ["analise"], "default": "analise" }
        },
        "example": {
          "uploadId": "up_4Zt9QmW2xLr7Kp1Vb8Nc3Hs6",
          "fileName": "files/a1b2c3d4e5",
          "duracaoSeg": 95,
          "regua": "vendas",
          "contexto": { "situacao": "Pitch de vendas", "publico": "Diretores de compras", "objetivo": "Agendar uma demonstração" },
          "consentimento": { "coletadoEm": "2026-10-30T13:58:00Z", "versaoTexto": "consentimento-v1" },
          "referencia": "usuario-4821"
        }
      },
      "ResultadoApi": {
        "type": "object",
        "required": [
          "id",
          "objeto",
          "ambiente",
          "simulado",
          "status",
          "criadoEm",
          "referencia",
          "duracaoSeg",
          "regua",
          "notaGeral",
          "faixa",
          "dimensoes",
          "cenario",
          "metricas",
          "relatorio"
        ],
        "description": "Fora de `pronta`, `notaGeral`, `faixa`, `cenario`, `metricas` e `relatorio` vêm null e `dimensoes` vem vazia. Trate campos desconhecidos como opcionais.",
        "properties": {
          "id": { "type": "string", "description": "No sandbox começa com `sb_`." },
          "objeto": { "const": "analise" },
          "ambiente": { "$ref": "#/components/schemas/Ambiente" },
          "simulado": { "type": "boolean", "description": "true no sandbox." },
          "status": { "$ref": "#/components/schemas/Status" },
          "criadoEm": { "type": "string", "format": "date-time" },
          "referencia": { "type": ["string", "null"] },
          "duracaoSeg": { "type": ["number", "null"] },
          "regua": {
            "type": "object",
            "required": ["id", "versao"],
            "properties": {
              "id": { "type": "string", "description": "A régua pedida." },
              "versao": { "type": ["string", "null"], "description": "Versão da régua universal usada na nota geral." }
            }
          },
          "notaGeral": { "type": ["integer", "null"], "minimum": 0, "maximum": 100, "description": "Nota de 0 a 100 na régua universal." },
          "faixa": { "type": ["string", "null"], "enum": ["Baixo", "Médio", "Bom", "Excelente", null] },
          "dimensoes": { "type": "array", "items": { "$ref": "#/components/schemas/Dimensao" } },
          "cenario": {
            "oneOf": [{ "$ref": "#/components/schemas/Cenario" }, { "type": "null" }],
            "description": "Só com régua de cenário. Pode vir null mesmo assim, quando a régua de cenário não pôde ser aplicada nesta análise (a nota geral e o relatório vêm normalmente)."
          },
          "metricas": { "oneOf": [{ "$ref": "#/components/schemas/Metricas" }, { "type": "null" }] },
          "relatorio": { "oneOf": [{ "$ref": "#/components/schemas/Relatorio" }, { "type": "null" }] }
        }
      },
      "Dimensao": {
        "type": "object",
        "required": ["id", "nome", "nota", "faixa"],
        "properties": {
          "id": { "$ref": "#/components/schemas/DimensaoId" },
          "nome": { "type": "string" },
          "nota": { "type": "integer", "minimum": 0, "maximum": 100 },
          "faixa": { "type": "string", "enum": ["Baixo", "Médio", "Bom", "Excelente"] }
        }
      },
      "Cenario": {
        "type": "object",
        "required": ["reguaId", "nome", "versao", "nota", "faixa", "criterios"],
        "properties": {
          "reguaId": { "type": "string" },
          "nome": { "type": "string" },
          "versao": { "type": "string" },
          "nota": { "type": "integer", "minimum": 0, "maximum": 100 },
          "faixa": { "type": "string", "enum": ["inicial", "em desenvolvimento", "consistente", "referência"] },
          "criterios": { "type": "array", "items": { "$ref": "#/components/schemas/Criterio" } }
        }
      },
      "Criterio": {
        "type": "object",
        "required": ["id", "nome", "nota", "escala"],
        "properties": {
          "id": { "type": "string" },
          "nome": { "type": "string" },
          "nota": {
            "type": ["integer", "null"],
            "minimum": 0,
            "maximum": 4,
            "description": "null quando o critério não pôde ser avaliado nesta fala."
          },
          "escala": { "const": 4 }
        }
      },
      "Metricas": {
        "type": "object",
        "required": ["palavrasPorMinuto", "totalPalavras", "abertura"],
        "properties": {
          "palavrasPorMinuto": { "type": ["integer", "null"], "description": "Ritmo médio da fala. null quando não houve fala." },
          "totalPalavras": { "type": ["integer", "null"] },
          "abertura": {
            "oneOf": [
              {
                "type": "object",
                "required": ["segundosAteIdeiaPrincipal", "estimado", "faixa"],
                "properties": {
                  "segundosAteIdeiaPrincipal": { "type": "integer", "minimum": 0 },
                  "estimado": { "const": true },
                  "faixa": { "type": ["string", "null"], "enum": ["direta", "adequada", "longa", null] }
                }
              },
              { "type": "null" }
            ]
          }
        }
      },
      "Relatorio": {
        "type": "object",
        "required": ["resumo", "pontosFortes", "ajustes", "momentos", "plano7Dias"],
        "properties": {
          "resumo": { "type": "string" },
          "pontosFortes": { "type": "array", "items": { "type": "string" } },
          "ajustes": { "type": "array", "items": { "type": "string" } },
          "momentos": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["inicioSeg", "categoria", "descricao"],
              "properties": { "inicioSeg": { "type": "integer", "minimum": 0 }, "categoria": { "type": "string" }, "descricao": { "type": "string" } }
            }
          },
          "plano7Dias": {
            "oneOf": [
              {
                "type": "object",
                "required": ["foco", "objetivo", "dias"],
                "properties": {
                  "foco": { "oneOf": [{ "$ref": "#/components/schemas/DimensaoId" }, { "type": "null" }] },
                  "objetivo": { "type": "string" },
                  "dias": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": ["dia", "titulo", "exercicio", "instrucoes", "duracaoMin"],
                      "properties": {
                        "dia": { "type": "integer" },
                        "titulo": { "type": "string" },
                        "exercicio": { "type": "string" },
                        "instrucoes": { "type": "string" },
                        "duracaoMin": { "type": "integer" }
                      }
                    }
                  }
                }
              },
              { "type": "null" }
            ]
          }
        }
      },
      "ListaAnalises": {
        "type": "object",
        "required": ["objeto", "dados", "temMais", "proximo"],
        "properties": {
          "objeto": { "const": "lista" },
          "dados": { "type": "array", "items": { "$ref": "#/components/schemas/ResultadoApi" } },
          "temMais": { "type": "boolean" },
          "proximo": { "type": ["string", "null"], "format": "date-time", "description": "Passe como `antesDe` para a próxima página." }
        }
      },
      "AnaliseApagada": {
        "type": "object",
        "required": ["id", "objeto", "apagada"],
        "properties": { "id": { "type": "string" }, "objeto": { "const": "analise" }, "apagada": { "const": true } }
      },
      "ReguaCatalogo": {
        "type": "object",
        "required": ["id", "nome", "descricao", "versao", "tipo", "aviso"],
        "properties": {
          "id": { "type": "string" },
          "nome": { "type": "string" },
          "descricao": { "type": "string" },
          "versao": { "type": "string" },
          "tipo": { "type": "string", "enum": ["universal", "cenario"] },
          "aviso": { "type": ["string", "null"], "description": "Restrição de uso, quando houver." }
        }
      },
      "ListaReguas": {
        "type": "object",
        "required": ["objeto", "dados"],
        "properties": { "objeto": { "const": "lista" }, "dados": { "type": "array", "items": { "$ref": "#/components/schemas/ReguaCatalogo" } } }
      },
      "Uso": {
        "type": "object",
        "required": ["objeto", "ambiente", "saldoCentavos", "saldoValidoAte", "precoAnaliseCentavos", "hoje", "limites"],
        "properties": {
          "objeto": { "const": "uso" },
          "ambiente": { "$ref": "#/components/schemas/Ambiente" },
          "saldoCentavos": { "type": "integer", "description": "Saldo disponível agora, em centavos de real (vencido conta como zero)." },
          "saldoValidoAte": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "Até quando o saldo vale: 12 meses a partir da compra. Compra nova com o saldo ainda válido soma e leva a validade de todo o saldo para 12 meses a partir dela (nunca encurta). Saldo vencido não volta com uma compra nova: sai como vencimento e só a compra nova entra."
          },
          "precoAnaliseCentavos": {
            "type": "integer",
            "description": "Quanto cada análise desconta do saldo com a chave usada. 0 com chave de teste."
          },
          "hoje": {
            "type": "object",
            "required": ["chamadas", "gastoCentavos", "tetoCentavos"],
            "description": "Da chave usada, no dia de Brasília.",
            "properties": { "chamadas": { "type": "integer" }, "gastoCentavos": { "type": "integer" }, "tetoCentavos": { "type": "integer" } }
          },
          "limites": {
            "type": "object",
            "required": ["requisicoesPorMinuto", "duracaoMaxSeg"],
            "properties": { "requisicoesPorMinuto": { "type": "integer" }, "duracaoMaxSeg": { "type": "integer" } }
          }
        }
      },
      "Evento": {
        "type": "object",
        "required": ["id", "objeto", "tipo", "criadoEm", "dados"],
        "properties": {
          "id": { "type": "string", "description": "Igual em todas as tentativas de entrega do mesmo evento. Use para descartar repetições." },
          "objeto": { "const": "evento" },
          "tipo": {
            "type": "string",
            "enum": ["analise.concluida", "analise.falhou", "analise.retida"],
            "description": "`analise.concluida`: ficou pronta. `analise.falhou`: não será entregue (status `falhou` ou `nao_entregue`; confira em `dados.status`). `analise.retida`: retida para conferência (`em_revisao`); depois chega `analise.concluida` ou `analise.falhou`."
          },
          "criadoEm": { "type": "string", "format": "date-time" },
          "dados": { "$ref": "#/components/schemas/ResultadoApi" }
        }
      }
    }
  }
}
