> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Feedback e roadmap público

> Um board que seus usuários escrevem, um roadmap que qualquer um lê, e um ranking pela receita por trás dos votos que nunca sai do painel.

Feedback é o único módulo aqui cujas linhas são escritas pelos **seus usuários** e não
por você. Três superfícies ficam sobre elas, e o que cada uma pode carregar é o
desenho.

## O board que seus usuários veem

```ts theme={null}
const { posts, statuses } = await userkit.getFeedbackBoard();
```

```tsx theme={null}
import { FeedbackButton } from "@userkit/react";

<FeedbackButton />;                          {/* o quadro abre no fluxo */}
<FeedbackButton position="center" />;        {/* só o botão fica no seu layout */}
<FeedbackButton position="bottom-right" />;
```

`position` aceita `inline` (padrão), `center`, `bottom-right`, `bottom-center` ou
`bottom-left` — as
mesmas cinco que a [pesquisa](/pt-br/guides/surveys) aceita, e um vocabulário só de
propósito: um produto que põe a pesquisa no canto e o quadro no centro está
descrevendo um layout, não dois.

Inline o quadro abre dentro do cartão, embaixo do título a que pertence. As três
posições flutuantes deixam **só o botão** onde você o colocou, que é o que as torna
úteis: um header ou um menu tem espaço para um botão e não para uma lista de ideias
com um formulário embaixo. Só o `center` escurece a página; os cantos flutuam sobre
uma página que continua legível e clicável, e o Esc fecha as três.

Num app Next, importe do `@userkit/nextjs` — é o mesmo componente, e em
[modo proxy](/pt-br/customer-auth/session-tokens) a leitura do board, o voto e a
submissão são encaminhados pelos handlers, com a sessão num cookie httpOnly da sua
própria origem.

O board fica atrás do portão de contato verificado, por uma razão que não é a do
changelog nem a da pesquisa: não é o que a resposta revela, é o que a **escrita** faz.
Enviar coloca as palavras de um estranho numa página que você publica com o seu nome,
e um voto é uma linha da qual o seu ranking de roadmap é construído — as duas são
afirmações que qualquer um que saiba um `external_id` poderia fazer de um navegador.

Sessões anônimas passam por esse portão, como passam em todo lugar, e então são
recusadas pelas duas escritas com `identity_required`: o widget mina uma por
carregamento de página, então um board que as contasse estaria contando recargas. Ler
é aberto a elas, que é o que o documento público já mostra de qualquer forma.

O board diz a esta pessoa em quais posts ela votou. Nunca diz o que aqueles votantes
pagam.

Uma pessoa pode escrever alguns pedidos por dia; além disso a API responde `429`, que é
"depois" e não "nunca".

## Cinco status

`open`, `planned`, `in_progress`, `shipped`, `declined` — e eles percorrem essa ordem,
que é por que o vocabulário volta na resposta em vez de ficar cravado num cliente.

Todo valor que não é `open` nomeia uma resposta diferente que você deu. Não existe um
sexto valor significando "duplicado": isso é `merged_into_id`, e mesclar é um ato e não
um status.

## O roadmap público

```http theme={null}
GET /v1/roadmap/{publishable_key}
```

```json theme={null}
{
  "posts": [
    { "id": "…", "title": "Exportação em massa", "body": "…", "status": "planned", "votes": 34, "created_at": "…" }
  ],
  "statuses": ["open", "planned", "in_progress", "shipped", "declined"],
  "max_age_seconds": 60
}
```

O nono documento cacheável, e o único cujas linhas foram escritas pelos seus usuários e
não por você — que é por que o que ele leva é deliberadamente estreito: o título, o
corpo, o status e uma **contagem simples de votos**. Não quem pediu, que é o seu dado
sobre o seu usuário. Não a receita por trás dos votos, que nunca sai do painel.

`body` é **texto puro escrito por um dos seus usuários**. Nunca renderize como markup.
É a única coluna deste plano que alguém de fora da sua equipe consegue colocar numa
página que você publica.

Toda leitura de board é limitada a 500 posts em vez de paginada. Esse limite é uma
afirmação de produto tanto quanto técnica — um board com quinhentos posts vivos tem um
problema de triagem e não de paginação — e ele precisa ser um número só nas três
leituras, porque o ranking é calculado sobre o conjunto que a lista devolveu.

## O peso

```http theme={null}
GET /v1/organization/feedback?environment=live&currency=usd&sort=weight
```

```json theme={null}
{
  "weight": {
    "currency": "usd",
    "amount_minor": 484000,
    "paying_customers": 11,
    "other_currency_customers": 4,
    "free_customers": 19
  }
}
```

O peso de um post é a soma do valor mensal das assinaturas ativas dos **clientes
distintos** por trás dos seus votantes — então um time de cinco vale a receita dele uma
vez, não cinco votos. Preços anuais são divididos por 12, preços avulsos nunca entram, e
um trial entra quando termina.

**Ele existe neste endpoint e em nenhum outro lugar.** Não no roadmap público, não no
board do próprio contato, não num webhook. É uma query separada que só a listagem de
staff faz, então uma resposta pública não consegue adquirir um peso porque alguém
adicionou um campo numa projeção compartilhada. Seus usuários podem saber quantas
pessoas querem algo; não podem saber o que essas pessoas pagam.

**A moeda é um argumento, nunca um palpite.** Não há tabela de câmbio aqui, então somar
unidades menores entre moedas produz um número que não é nenhuma delas — pedir a ordem
por peso sem nomear uma moeda é `400` e não um padrão. E a resposta relata quantos dos
clientes por trás de cada post pagam em outra moeda ou em nada, porque um peso de zero
ao lado de quatro `other_currency_customers` diz algo completamente diferente de um peso
de zero ao lado de quatro `free_customers`. Um número solto não diria nenhum dos dois, e
uma tranquilização é exatamente o que faz alguém parar de conferir.

## Mesclando duplicados

```http theme={null}
POST /v1/organization/feedback/{id}/merge
```

Os votos **movem** para o sobrevivente com `ON CONFLICT DO NOTHING`, então quem votou
nos dois conta uma vez. `merged_into_id` registra para onde o post foi, e cada voto
movido registra de qual post ele veio.

Não existe endpoint de desfazer merge, e não precisa existir: as duas metades da
proveniência ficaram, então um merge errado é reparável à mão. `feedback_post.merged` é
entregável justamente porque muda a contagem de votos de um post que ninguém editou —
um espelho do seu board que só tivesse ouvido "criado" e "status mudou" manteria o
duplicado para sempre e estaria errado sobre o total no sobrevivente.

## Avisando quem votou que saiu

Mire um [post de changelog](/pt-br/guides/changelog) no post de feedback em vez de num
segmento:

```json theme={null}
{ "title": "Exportação em massa chegou", "feedback_post_id": "…" }
```

A audiência é um **JOIN**, não uma lista de destinatários copiada: quem vota depois de
você publicar recebe, quem retira o voto para de receber. Como todo post direcionado,
ele fica fora do documento público do changelog.

## O que sai do prédio

`feedback_post.created`, `feedback_post.status_changed`, `feedback_post.merged` e
`feedback_vote.cast` são todos entregáveis como [webhooks](/pt-br/guides/webhooks).

`status_changed` leva **os dois** status, para você agir sobre a transição em vez de
comparar dois payloads — o barramento não é ordenado, então "o anterior" não é algo que
um consumidor consiga reconstruir. Definir um status que o post já tem não anuncia nada.

`feedback_vote.cast` leva o post e o contato e **nunca o peso**.

## Permissões

`feedback:read` é de owner, admin **e member** — triagem é trabalho de suporte, e o
assento member é o agente de suporte. `feedback:write` — responder, mesclar, registrar
em nome de alguém — é de owner e admin.

É o único lugar deste conjunto de módulos em que a leitura é separada da escrita.
Changelog, checklists e pesquisas compartilham um `engagement:manage`, porque operar o
que seus usuários veem é um ato; um board de feedback é algo que sua mesa de suporte lê
o dia inteiro e raramente muda.
