# Esteira Cenbrap — manual da API

Você está lendo o contrato da API da Esteira Cenbrap, o quadro de produção de
conteúdo do CENBRAP. Humanos operam pelo quadro em https://esteira.cenbrap.edu.br; agentes operam por
esta API. É o mesmo dado e a mesma regra dos dois lados — o que você fizer aqui
aparece na tela de quem está trabalhando, e vice-versa.

Base: https://esteira.cenbrap.edu.br
Especificação OpenAPI (JSON): https://esteira.cenbrap.edu.br/api/openapi
Este manual: https://esteira.cenbrap.edu.br/api/docs

## Como se autenticar

Toda rota (menos /api/docs e /api/openapi) exige a sua chave em um header:

    x-api-key: SEU_TOKEN

ou, se preferir:

    Authorization: Bearer SEU_TOKEN

Cada agente tem o seu próprio token, e a API registra o seu nome em tudo que você
faz. Não use o token de outro agente: o histórico ficaria errado. Sem chave, a
resposta é 401 sem_credencial.

Este header é o caminho dos AGENTES. Pessoas não usam token: elas entram no site
com e-mail e senha e recebem um cookie de sessão. As duas portas dão no mesmo
dado e obedecem às mesmas regras; o que muda é o nome que fica no histórico.

## O modelo mental (leia antes de chamar qualquer coisa)

FUNIL é um quadro inteiro. A Esteira tem vários, e cada um tem as SUAS PRÓPRIAS
etapas. O funil do Blog pode ir de pauta a publicado; um funil de YouTube pode ir
de ideia a no-ar. Nunca presuma que as etapas de um funil valem para outro.

ETAPA é a coluna onde o card está, dentro do funil dele. O identificador da etapa
só existe dentro daquele funil.

TEMA é o card: um assunto que vai virar conteúdo. Ele mora em um funil, está em
uma etapa e tem prazo, responsável e prioridade.

PEÇA é cada item do enxoval do tema — o Reels, o carrossel, o e-mail. Um tema tem
várias peças, e cada uma é marcada como feita ou não.

AÇÃO é a decisão estratégica que está ACIMA de tudo isso, no Plano de Ação. Ela é a
origem: cada ação mantém UM tema, e os canais dela viram as peças desse tema. Nem
todo tema vem de uma ação (dá para criar card solto), mas todo tema que tem acao_id
é comandado por uma. Antes de reescrever um card desses, entenda a regra abaixo.

ESTÁGIO (o campo estagio) é topo, meio ou fundo de funil de marketing. NÃO confunda
com funil_id, que é o quadro. O campo já se chamou "funil" e foi renomeado para
"estagio" justamente por causa dessa confusão. Se você mandar "funil", a API ainda
aceita como estagio, mas escreva "estagio".

## A regra dura: validação antes de andar

Cada funil aponta em trava_a_partir_de a etapa em que a trava começa. Dali para a
frente, um tema só se move se tiver as duas validações marcadas:

  val_tecnica      — a precisão clínica, conferida por um especialista
  val_regulatoria  — publicidade médica e compliance liberados

Se faltar alguma, a API responde 409 e diz exatamente o que falta:

    {"erro":"validacao_pendente","falta":["tecnica","regulatoria"],"mensagem":"..."}

Isso não é sugestão: a API não move o card. Se você receber esse 409, o caminho é
marcar as validações (PATCH no tema) ou avisar um humano — nunca tentar de novo do
mesmo jeito. Um funil pode não ter trava, quando trava_a_partir_de vem vazio.

A última etapa de cada funil é a de "pronto": ao chegar nela, a API carimba
concluido_em sozinha. Você não precisa preencher esse campo.

## Por onde começar, sempre

    GET /api/etapas

Devolve o vocabulário vivo: os funis que existem, as etapas de cada um, onde a
trava começa, e quem a API achou que você é. Chame isto primeiro em toda sessão.
Os ids de etapa que você vai usar nas outras chamadas saem daqui — não os invente
e não os traduza.

As etapas que um funil novo recebe, se ninguém disser outra coisa:

  criacao     Criação — Tema, público e ângulo definidos
  validacao   Validação — Precisão clínica e compliance
  programacao Programação — Conteúdo aprovado para entrar na agenda
  no-ar       No ar — Publicado nos canais

## Endpoints

### Vocabulário

GET /api/etapas
    Os funis, as etapas de cada um, a trava e a sua identidade.

GET /api/vocabulario
    As listas editáveis do Plano de Ação: responsáveis, canais e cursos/frentes.
    São a fonte dos nomes que você escreve em responsavel, canais[].nome e
    cursos de uma ação. Use estes valores em vez de inventar: nome fora da lista
    é aceito pela API, mas some dos filtros da tela e vira sinônimo perdido.
    Vem tudo de uma vez, cada lista já na ordem em que a tela mostra:
      {"tipos":[responsavel, canal, curso],
       "vocabulario":{"responsavel":[{"id":"...","valor":"Murilo","ordem":1000}],
                      "canal":[...],"curso":[...]}}
    Filtros: tipo=canal (ou vários, separados por vírgula) e arquivados=true.

POST /api/vocabulario
    Corpo: {"tipo":"canal","valor":"Telegram"}. Entra no fim da lista.
    Se o nome já existe e está em uso, 409. Se existe mas foi removido um dia, a
    API ressuscita a linha antiga e responde 200 com "restaurado":true — assim as
    ações que já citam esse nome continuam casando com ele.

PATCH /api/vocabulario/<id>
    {"valor":"..."} renomeia, {"ordem":2500} reposiciona, {"arquivado":false}
    restaura. O tipo não muda: um item não troca de lista.

DELETE /api/vocabulario/<id>
    Arquiva. Nada é apagado, porque as ações guardam o NOME e o histórico precisa
    continuar fazendo sentido depois que a lista muda.

POST /api/vocabulario/ordem
    {"tipo":"canal","ids":["<id>","<id>",...]} na ordem desejada. A API renumera
    a lista inteira. É o que a tela usa ao arrastar — evite PATCHes de ordem um a
    um, que deixam a lista inconsistente se um deles falhar.

### Funis (os quadros)

GET /api/funis
    Lista os funis com contagem de temas e de atrasados.

POST /api/funis
    Cria um funil. Corpo: {"nome":"YouTube","etapas":[{"nome":"Ideia"},...],
    "trava_a_partir_de":"gravacao"}. Sem "etapas", nasce com as seis padrão.
    O id de cada etapa é gerado a partir do nome quando você não manda um.
    Máximo de 12 etapas.

GET /api/funis/<id>
    O funil e todos os temas dele de uma vez. É a chamada mais eficiente quando
    você vai trabalhar em um quadro inteiro.

### Temas (os cards)

GET /api/temas
    Filtros por querystring, combináveis:
      funil_id      só de um quadro
      etapa         só de uma coluna
      responsavel   por dono
      prioridade    alta | media | baixa
      busca         texto livre no título
      atrasados     true, para os que passaram do prazo
      ativos        true, para os que ainda não chegaram na última etapa
      desde         data-hora ISO 8601; devolve só o que foi criado ou alterado
                    depois dela. É o filtro de uma ronda periódica: guarde a hora
                    da última verificação e passe aqui. Vazio significa que nada
                    mudou — e nada a fazer.

GET /api/temas/<id>
    Um tema com o histórico do que já aconteceu com ele.

POST /api/temas
    Cria um tema. Obrigatório: titulo. Recomendado mandar junto funil_id,
    publico, objetivo, cta, estagio, prioridade, prazo e responsavel.
    Sem funil_id, o tema cai no primeiro funil ativo.

PATCH /api/temas/<id>
    Atualiza os campos que você mandar, e só esses. Serve para marcar as
    validações, mudar prazo, trocar responsável — e para mover o card para outra
    esteira, mandando um funil_id diferente.

POST /api/temas/<id>/mover
    Corpo: {"etapa":"copy"}. Move dentro do funil do tema. Responde 409 se a
    trava pegar.

POST /api/temas/<id>/copiar
    Corpo: {"funil_id":"<id>","manter_pecas":false}. O original fica onde está;
    a cópia entra na primeira etapa do destino.

POST /api/temas/<id>/peca
    Corpo: {"canal":"Reels","feito":true}. Marca uma peça do enxoval.

### Ações (o plano que comanda a esteira)

Quem manda em quê — leia isto antes de mexer num tema que tem acao_id:

  - o CONTEÚDO do card (titulo, objetivo, publico, responsavel, prioridade, prazo e
    a lista de peças) é escrito pela AÇÃO toda vez que ela é salva. Se você editar
    esses campos direto no tema, a próxima gravação da ação passa por cima;
  - a ETAPA é da esteira. A ação só escolhe a etapa de entrada, quando o card nasce.
    Depois disso quem move o card é quem trabalha no quadro — inclusive você;
  - o STATUS da ação SEGUE a etapa do card, sozinho: primeira etapa = nao-iniciada,
    etapa de validação = aguardando-validacao, última = concluida. A exceção é
    "bloqueada", que é decisão humana e nenhuma movimentação apaga;
  - peça já marcada como feita CONTINUA feita quando a ação é salva de novo.

GET /api/acoes
    Filtros: status, responsavel, prioridade, busca, atrasadas=true,
    arquivadas=true, limite. Cada ação vem com "card": onde o trabalho dela está
    na esteira (funil, etapa, etapa_nome e o enxoval feitas/total).

POST /api/acoes
    Corpo mínimo: {"titulo":"..."}. Sem funil_id, entra no primeiro funil.
    Criar a ação JÁ CRIA o card no funil escolhido, com os canais como peças.

GET /api/acoes/<id>
PATCH /api/acoes/<id>
    Só os campos que mudam. Alterar a ação reflete no card na mesma chamada.
    Mudar funil_id leva o card junto: ele entra pela primeira etapa do funil novo,
    porque a etapa antiga pode não existir lá.
DELETE /api/acoes/<id>
    Arquiva dos dois lados: a ação sai do plano e o card sai do quadro.

Campos de uma ação:

  titulo                obrigatório na criação
  objetivo_final        o resultado de negócio que ela persegue
  descricao             contexto da frente
  conteudo_mae          o texto que origina o enxoval
  conteudo_mae_link     link para ele
  investimento          texto livre: "R$ 5.000,00" ou "horas da equipe"
  prioridade            alta | media | baixa
  responsavel           nome de quem toca
  status                nao-iniciada | em-andamento | aguardando-validacao |
                        concluida | bloqueada
  prazo                 AAAA-MM-DD ou null
  cursos                lista de nomes; vira o "publico" do card
  canais                [{nome, modo, nota, copy}], modo = organico|pago|ambos;
                        cada canal vira uma peça do enxoval
  validacoes            [{item, feito, por}]
  links                 lista de URLs
  observacoes           dependências, bloqueios, próximos passos
  funil_id              em qual funil da esteira o card desta ação vive

### Anexos

O arquivo não passa pela Vercel: a API assina uma URL e você sobe direto para o
Storage. São três passos, nesta ordem:

  1. POST /api/temas/<id>/anexos      (arquivo do card)
     ou POST /api/acoes/<id>/anexos   (arquivo do plano de ação)
     Corpo: {"nome":"capa.png","tipo":"image/png","tamanho":12345}
     Devolve o registro e uma URL de upload assinada.
  2. PUT na URL que voltou, com o arquivo cru no corpo.
  3. PATCH /api/anexos/<id do anexo> com {"pronto":true}

Só depois do passo 3 o anexo aparece na listagem — um upload interrompido não vira
arquivo fantasma. Os links assinados que a listagem devolve valem 1 hora. Para
remover: DELETE /api/anexos/<id do anexo>, venha o arquivo de onde vier.

Anexo de ação e anexo de card são A MESMA COISA, vista de dois lugares. Não existe
um segundo sistema de arquivos: toda ação mantém um card, o arquivo mora no card, e
o campo escopo diz de qual gaveta ele veio:

  tema | acao | conteudo-mae | canal

  tema          subido no card, pela esteira. É o padrão de POST /api/temas/<id>/anexos
  acao          subido no Plano de Ação, na ação em si (briefing, referências)
  conteudo-mae  subido no Plano de Ação, junto do campo conteudo_mae

GET /api/temas/<id>/anexos
    Tudo que é material daquela campanha — os três escopos juntos. É de propósito:
    quem está gravando o Reels precisa ver o briefing sem trocar de tela. Restrinja
    com ?escopo=tema (ou vários, separados por vírgula) se quiser só uma gaveta.

GET /api/acoes/<id>/anexos
    Tudo que está preso a esta ação, já separado por gaveta:
    {"acao_id":"...","tema_id":"...","total":3,"anexos":[...],
     "por_escopo":{"acao":[...],"conteudo-mae":[...],"tema":[...]}}
    "tema" vem preenchido quando alguém subiu arquivo direto no card — o plano
    pode mostrar ou ignorar, mas nunca fica sem saber que existe.

POST /api/acoes/<id>/anexos
    Corpo: {"nome":"...","tipo":"...","tamanho":123,"escopo":"acao"}.
    escopo aqui só aceita acao ou conteudo-mae; sem escopo, assume "acao".
    Se a ação estiver sem card (funil apagado, por exemplo), a API recria o card
    antes de assinar o upload em vez de recusar o arquivo.

Campos de um anexo: id, nome, tipo (MIME), tamanho (bytes), escopo, tema_id,
acao_id, pronto, criado_em, criado_por, e nas listagens url (assinada, 1h) e
e_imagem.

## Os campos de um tema

  id                    uuid, gerado pela API
  titulo                obrigatório na criação
  publico               para quem é este conteúdo
  funil_id              uuid do quadro onde o tema vive
  etapa                 id de uma etapa DO FUNIL do tema
  estagio               topo | meio | fundo
  objetivo              o que este conteúdo precisa provocar
  cta                   a chamada para ação desejada
  prioridade            alta | media | baixa
  prazo                 AAAA-MM-DD, ou null
  responsavel           texto livre
  val_tecnica           booleano
  val_tecnica_resp      quem validou
  val_regulatoria       booleano
  val_regulatoria_resp  quem liberou
  pecas                 lista de {canal, feito}, até 40
  arquivado             booleano
  concluido_em          carimbado pela API na última etapa

Campos calculados que já vêm prontos em toda resposta de tema. Não recalcule:

  enxoval               {feitas, total} das peças
  dias_para_o_prazo     negativo quando já passou
  atrasado              booleano
  vence_em_5_dias       booleano
  ativo                 ainda não chegou na última etapa
  liberado_para_copy    as duas validações marcadas

## Erros

  400 dados_invalidos     o corpo tem campo fora do vocabulário; o corpo da
                          resposta traz a lista do que foi recusado e por quê
  401 sem_credencial      falta o header x-api-key
  404 nao_encontrado      id que não existe
  409 validacao_pendente  a trava do funil; o campo "falta" diz o que falta

Todo erro vem em JSON com "erro" e "mensagem". Leia a mensagem: ela foi escrita
para dizer o que fazer, não só o que houve.

## Receitas

Pegar o trabalho atrasado de um quadro:

    GET /api/etapas                       (descobre o funil_id)
    GET /api/temas?funil_id=<id>&atrasados=true

Levar um tema da validação para a copy:

    PATCH /api/temas/<id>  {"val_tecnica":true,"val_tecnica_resp":"Dr. Fulano"}
    PATCH /api/temas/<id>  {"val_regulatoria":true,"val_regulatoria_resp":"Jurídico"}
    POST  /api/temas/<id>/mover  {"etapa":"copy"}

Abrir uma pauta nova já com o enxoval previsto:

    POST /api/temas
    {"funil_id":"<id>","titulo":"...","publico":"...","estagio":"topo",
     "objetivo":"...","cta":"...","prioridade":"media","prazo":"2026-09-30",
     "responsavel":"Equipe de Conteúdo",
     "pecas":[{"canal":"Reels","feito":false},{"canal":"Carrossel","feito":false}]}

Fechar uma peça do enxoval:

    POST /api/temas/<id>/peca  {"canal":"Reels","feito":true}

Escrever uma ação com os nomes que a equipe usa (e não sinônimos seus):

    GET  /api/vocabulario                 (responsáveis, canais e cursos)
    POST /api/acoes  {"titulo":"...","responsavel":"<um dos responsavel>",
                      "cursos":["<um dos curso>"],
                      "canais":[{"nome":"<um dos canal>","modo":"organico"}]}

Subir o briefing de uma ação (o arquivo aparece também no card dela):

    POST  /api/acoes/<id>/anexos  {"nome":"briefing.pdf","tipo":"application/pdf",
                                   "tamanho":98765,"escopo":"acao"}
    PUT   <upload.url que voltou>   (o arquivo cru)
    PATCH /api/anexos/<id do anexo>  {"pronto":true}

## Como se comportar aqui

Comece por GET /api/etapas e use os ids que vierem de lá. Nunca invente id de
etapa nem traduza o nome dela.

Peça só o que vai usar: prefira GET /api/funis/<id>, que traz o quadro inteiro em
uma chamada, a varrer os temas um a um.

Ao receber 409, pare e resolva a validação — não insista na mesma chamada.

Escreva em PATCH apenas os campos que você realmente quer mudar. O que você não
mandar fica como está, e isso é proposital: dois agentes podem trabalhar no mesmo
card sem um apagar o outro.

Você está mexendo no trabalho de pessoas. Quando a situação não estiver clara —
um tema sem responsável, um prazo impossível, uma validação que ninguém marcou —
registre o que encontrou e avise um humano em vez de decidir sozinho.
