> ## 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.

# Changelog

> Notas de versão em três superfícies — uma página pública, um feed direcionado e um badge de não lidos — e por que a pública abre mão do direcionamento.

Um post de changelog é escrito como rascunho, mirado em todo mundo ou numa
audiência, e publicado por um ato próprio. Três superfícies respondem três perguntas
diferentes sobre as mesmas linhas, e as diferenças entre elas são o desenho inteiro.

## Publicar é um ato, não uma coluna

```http theme={null}
POST /v1/organization/changelog/{id}/publish
```

`published_at` é um momento e não uma flag: a lista é ordenada por ele, e o badge de
não lidos compara a chegada de um contato contra ele. Publicar tem endpoint próprio
porque é o passo que entra num documento cacheável e anuncia
`changelog_post.published` para os seus [webhooks](/pt-br/guides/webhooks) — e um
formulário enviado duas vezes não pode anunciar um release duas vezes. O UPDATE leva
`published_at IS NULL`, então essa idempotência é do banco e não de uma checagem que
alguém lembrou de escrever.

Editar um post publicado é uma edição; muda o documento e não anuncia nada. Os fatos
do rascunho ficam dentro do prédio: um post não publicado é um anúncio que você
ainda não fez.

## O documento público

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

```json theme={null}
{
  "posts": [
    { "id": "…", "title": "Exportação em massa", "body_markdown": "…", "published_at": "2026-07-31T09:00:00Z" }
  ],
  "max_age_seconds": 60,
  "generated_at": "2026-08-02T12:00:00Z"
}
```

Cacheável, endereçado pela chave que já está na sua página, seguro atrás de um CDN, e
leva os **50 posts mais recentes**. Limitado em vez de paginado: um cursor num
documento cacheável multiplica as entradas de cache pelo número de posições em que um
leitor pode estar, e o que uma página de releases desenha são os releases recentes. O
histórico inteiro continua disponível na leitura do próprio contato, que não é
cacheada.

**O documento público abre mão do direcionamento, e isso é deliberado.** Uma
publishable key fica no HTML da página, então este documento não tem leitor para
resolver. Honrar `segment_id` aqui exigiria um identificador que quem chama fornece —
e um documento que responde "aqui está o post para este endereço de e-mail" é um
oráculo de quem está em qual [segmento](/pt-br/guides/segments), respondível por
qualquer um com uma página e uma lista de endereços. Pertencer a um segmento é a sua
própria análise sobre os seus usuários; um documento cacheável não pode ser sondado
atrás disso.

Então só um post mirado em **ninguém em particular** chega a este documento. Um post
direcionado é alcançável exclusivamente por uma sessão de contato.

`body_markdown` é markdown como foi escrito. **Renderizar é da página que desenha** —
a API nunca te devolve HTML, e você nunca deve entregar texto de banco a um
renderizador de markup que confia nele.

## O feed do próprio contato

```ts theme={null}
const posts = await userkit.listChangelogPosts();
const { unread, since } = await userkit.getChangelogUnread();
await userkit.markChangelogRead(id);
```

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

<ChangelogBadge />;
```

Num app Next, importe do `@userkit/nextjs` — é o mesmo componente, e em
[modo proxy](/pt-br/customer-auth/session-tokens) a leitura e o recibo de leitura são
encaminhados pelos handlers, com a sessão num cookie httpOnly da sua própria origem.
O `<WhatsNew />` mais abaixo e o `useChangelogFeed` atravessam pela mesma porta.

Esta é a superfície que carrega os posts direcionados, e ela fica atrás do **portão
de contato verificado** exatamente pela razão que faz o documento público abrir mão do
direcionamento. Uma sessão não verificada mas *identificada* é uma afirmação que
qualquer um que saiba um `external_id` pode fazer de um navegador, e a resposta deste
endpoint difere por pertencimento a segmento. Sem o portão, seria o mesmo oráculo
alcançado pela outra porta: "esse `external_id` vê o post do beta enterprise" é "essa
pessoa está no segmento do beta enterprise".

Sessões anônimas passam pelo portão, como passam em todo lugar — a linha foi criada
pela mesma chamada que a está segurando, então não há ninguém para personificar. O
que elas veem são os posts não direcionados, com estado de leitura.

## O badge conta a partir da chegada

```json theme={null}
{ "unread": 3, "since": "2026-07-15T14:02:00Z" }
```

`since` é quando este contato chegou, e **nada publicado antes disso conta**. Um badge
que abre em 47 no primeiro dia de alguém é um badge que a pessoa dispensa uma vez e
nunca mais olha. O número ser pequeno o bastante para agir é a única coisa que faz
dele um badge em vez de decoração.

Marcar um post como lido é idempotente — ler duas vezes é ler uma — e um post que esta
pessoa **nunca viu** responde `404` em vez de deixar um badge ser limpo por uuid.

## O permalink

```
GET /v1/changelog/{publishable_key}/{slug}
```

Todo post tem um **endereço**, e é por ele que o post é linkado: do widget, de uma
resposta de suporte, de um e-mail. O slug é gerado do título quando o post é escrito —
com os acentos dobrados para a letra base, porque `Exportações` virando `exporta-es` é
um endereço que ninguém lê — e é único **por ambiente**, pela mesma razão de todo
`environment_id` aqui: um ensaio no test não pode tomar o endereço que o gêmeo live ia
usar.

**Trocar o título não move a página.** Essa é a promessa inteira de um permalink: os
links já enviados continuam apontando para o post, e nada os reenvia. Mover o endereço
é mandar `slug` no PATCH, de propósito, e custa o endereço antigo.

Um rascunho e um post direcionado respondem **404 aqui**, com a mesma frase de um slug
que não existe. Distinguir os três responderia "existe um post não publicado neste
endereço" para qualquer um que adivinhe uma manchete — e numa superfície pública o
palpite é de graça. Um post mirado num segmento **guarda o slug**: ampliá-lo para todo
mundo depois liga aquele endereço em vez de inventar um.

O prefixo para montar essas URLs é `changelog_url`, em `GET /v1/config/{publishable_key}`
— resolvido por nós, porque só este lado sabe se o domínio próprio do inquilino foi
verificado. Na lista do contato, cada post carrega `slug` e `public`: **só ofereça o
link quando `public` for true**, ou você está oferecendo um 404.

## "Enquanto você esteve fora"

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

<WhatsNew />;
```

O mesmo changelog pelo outro lado. O badge espera ser clicado; este **abre sozinho**,
para o contato que está logado, com o que ele não viu — e o render comum é **nada**.

É a ordem das duas perguntas que garante isso. Ele lê `/v1/contact/changelog/unread`
primeiro e, num zero, acaba ali: a lista não é pedida, nenhum diálogo é montado, e quem
está em dia recebe a própria página de volta intacta. Um modal que cumprimenta alguém
com "nada de novo" é um modal que a pessoa aprende a fechar antes de ler, e quem paga
essa conta é o próximo anúncio de verdade.

O conjunto que ele mostra é o do próprio contador, repetido do lado do cliente: não
lido **e** publicado depois de `since`. A lista responde com tudo o que aquele contato
pode ver, inclusive o que é anterior a ele; o contador conta a partir da chegada. Ou os
dois números descrevem o mesmo conjunto, ou um deles está mentindo — e o que dá para
conferir é o que está na tela. Quem chegou hoje tem conjunto vazio por construção.

**Ser mostrado é o que marca esses posts como lidos**, senão o mesmo diálogo volta
amanhã com os mesmos posts. Os recibos são escritos depois que o diálogo está na tela,
um por post desenhado, e um recibo que falha custa uma reaparição — não um post.

E "mostrado" quer dizer na tela mesmo: o centro da página comporta **um diálogo por
vez** — um termo pendente vem antes deste, uma pesquisa vem depois —, e enquanto este
espera a vez nada é marcado como lido. Ele aparece assim que o de cima sair, ou na
próxima visita. A ordem está em [Pesquisas](/pt-br/guides/surveys), com a página que
explica a fila.

Diferente do badge, o corpo aqui é renderizado como **markdown**, pelo mesmo renderer
da central de ajuda: ele constrói elementos e nunca HTML, então a prosa que chega pela
rede não tem onde injetar. Uma nota de release lida num diálogo é prosa que alguém
escreveu para ser lida; a linha de preview do badge não tinha o que ganhar com títulos
e listas, e esta tem.

## A segunda audiência: quem pediu

Um post pode ser mirado num segmento, ou nos **votantes de um post de feedback**:

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

Os dois são mutuamente exclusivos, e qualquer um deles mantém o post fora do documento
público.

A audiência é um **JOIN, não uma lista de destinatários copiada**, o que é o que a faz
se comportar certo nas duas pontas: quem vota depois de você publicar ainda recebe, e
quem retira o voto para de receber. Veja
[feedback e roadmap](/pt-br/guides/feedback) para a outra metade.

## O que sai do prédio

`changelog_post.published` é entregável como webhook. O payload leva o título e o
momento e **nunca a audiência** — `segment_id` é o único campo que o documento público
recusa, e um webhook entregando isso seria o mesmo vazamento por outra porta.

Editar e excluir um post não são entregues.

## Permissões

`engagement:manage` — owner e admin por padrão, compartilhada com checklists e
pesquisas, porque operar o que seus usuários veem dentro do seu produto é um ato e não
três.
