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

# Checklists de onboarding

> Uma lista cujos passos são satisfeitos por fatos e não por marcação manual, e um funil que diz qual número ele não pode dar.

Uma checklist é o painel de "primeiros passos" dentro do seu produto: alguns
passos, cada um feito ou não, com um funil de conclusão atrás dizendo onde as
pessoas param.

O que ela tem de incomum é o que você **não** pode fazer: não existe endpoint que
marque um passo como feito.

## Um passo é satisfeito, nunca marcado

Três tipos, e **nenhum deles aceita escrita**. O que cada um é, e o que cada um
garante, está [mais abaixo](#o-que-satisfaz-um-passo) — esta seção é sobre a
regra que os três compartilham.

Não existe caminho de escrita contra o progresso, e essa ausência é o desenho.
Uma marcação que o cliente de alguém pode enviar é uma checklist que diz "você
conectou seu domínio" para quem não conectou, porque uma requisição foi repetida
ou um botão foi clicado por otimismo. Um passo satisfeito pelo fato que o
satisfaz é um passo que não pode estar errado.

Vale para o clique também. Um passo pode carregar um
[destino](#para-onde-um-passo-leva) e alguém pode apertá-lo — e apertar não
satisfaz nada. O link é a porta, não a prova de que alguém atravessou.

O corolário: **o que você quer que apareça numa checklist precisa ser um fato
antes.** Um passo nomeia um fato sobre uma PESSOA — um contato ou um vínculo, ou
um evento seu. Um fato sobre um time ou uma assinatura teria que ser espalhado
pelo quadro inteiro para ser atribuído, e "todo mundo completou *configurar
cobrança* porque uma pessoa completou" não é como um funil é lido.

## Ela conta a partir do momento em que é ligada

Um fato publicado antes de a checklist existir não conta. O barramento não é um
event store, então não há o que reproduzir — e uma regra que preenchesse alguns
passos retroativamente e outros não produziria um funil cujos números não podem ser
comparados entre si.

Na prática: crie a checklist, ligue, e leia o funil contra a coorte que chegou
depois. Uma checklist ligada hoje não conta o que os usuários do ano passado
fizeram.

`active` é um interruptor reversível e não um arquivamento, porque nada fora desta
API nomeia uma checklist por id.

## Renderizando

```ts theme={null}
const checklists = await userkit.getChecklist();
```

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

<OnboardingChecklist />;                          {/* no dashboard */}
<OnboardingChecklist position="bottom-right" />;  {/* um cartão no canto */}
```

`position` aceita `inline` (padrão), `center`, `bottom-right`, `bottom-center` ou
`bottom-left` — as
mesmas cinco que a [pesquisa](/pt-br/guides/surveys) e o
[quadro de sugestões](/pt-br/guides/feedback) aceitam.

**Dois painéis flutuantes nunca dividem um canto.** Um guia e um `<HelpWidget />` no
mesmo canto se cobriam, e o único conselho era pôr um à esquerda — uma decisão de
layout tomada no seu lugar para contornar um bug nosso. Agora o canto comporta um
painel: abrir a ajuda pede ao guia que saia de lado, e fechar a ajuda o devolve.

O guia sai de lado **saindo mesmo**, e o que torna isso seguro é a volta: um guia que
sumisse quando alguém abre a ajuda leria como a ajuda ter fechado o onboarding da
pessoa, enquanto um que volta no instante em que a ajuda fecha lê como o canto sendo
compartilhado. Recolher era a alternativa óbvia e não resolve — um card no mesmo canto
continua no canto.

A ajuda não volta no sentido contrário: alguém a abriu, algo aberto depois tomou o
canto, e uma janela de suporte que reaparecesse sozinha seria uma janela que a pessoa
fechou.

Flutuando, o cabeçalho carrega dois gestos, e eles são de fato diferentes.

**Recolher** encolhe o guia para um card — o título, a barra de progresso e a única
coisa a fazer em seguida. Não é um widget menor no canto; é o mesmo widget dizendo
menos, e ele volta a abrir pelo próprio controle.

**Fechar** tira da tela e **não deixa nada flutuando**. Um painel que responde
"fechar" com um painel menor no mesmo canto não fechou. O caminho de volta é um
controle que você posiciona:

```tsx theme={null}
<header>
  <Logo />
  <OnboardingLauncher />
</header>

<OnboardingChecklist position="bottom-right" />
```

O `<OnboardingLauncher />` desenha o mesmo progresso do guia e o abre. Ele não desenha
nada até um `<OnboardingChecklist />` na página dizer que há o que abrir, então nunca
vira um botão que não abre nada — e nada enquanto o guia flutuante **já** está aberto,
porque um painel sobre a página e um pill no header dizendo a mesma coisa é um a mais.
Passe `whileOpen` para mantê-lo nos dois estados, numa barra que reflowaria de forma
visível a cada aparição.

Um guia **inline** nunca o esconde: inline vive no fluxo da página e pode estar três
telas abaixo, então um pill que sumisse por causa dele não deixaria caminho de volta.

Do seu código, `onboardingGuide.show()` faz o mesmo.

### O que dá para tirar do pill

```tsx theme={null}
<OnboardingLauncher
  count={false}              // esconde o "6 de 9"; o anel continua dizendo a fração
  border={false}           // sem moldura e sem fundo
  label="Primeiros passos"   // outro texto, só aqui
/>
```

`count` é para um header já denso, ou um onde o pill fica ao lado de outros números
e um segundo compete com eles. O número continua indo para o leitor de tela — quem
não vê o anel não fica sem a informação.

`border={false}` é para uma barra que já tem a própria moldura, onde mais um
contorno é uma caixa em volta de algo que ninguém pediu para separar. O padding
lateral sai junto, porque ele existia para afastar o texto de uma borda que não é
mais desenhada.

`label` é para quando o dicionário viraria dois dicionários: o mesmo produto
chamando de "Configuração" num header e "Primeiros passos" numa página de ajustes.
Se for o nome em toda parte, o lugar é `localization` no provider — ele alcança
todas as superfícies de uma vez.

### Os gestos, e até onde eles vão

**Minimizar e fechar são lembrados pela visita.** Os dois ficam em
`sessionStorage`, com a chave do contato: um F5 não desfaz — é a mesma visita — e
fechar a aba desfaz, porque quem volta na semana seguinte merece o guia e não um
card que minimizou e esqueceu. A chave leva o contato porque `sessionStorage` é por
origem e não por pessoa; sem isso, num computador compartilhado o gesto de um vira
o estado do outro.

São um gesto só em duas profundidades, e por um tempo só o menor sobreviveu ao F5 —
que é a metade mais estranha de explicar: um reload devolvia por cima da página o
card que a pessoa acabara de tirar dali. Reabrir pelo seu próprio botão esquece o
fechar na hora; a decisão mais recente é a que vale.

**Ceder o canto não conta como fechar.** Quando o painel de ajuda ocupa o mesmo
canto, o guia sai de cena e volta quando a ajuda fecha — um recuo nosso, não uma
resposta de quem está lendo, e um F5 no meio disso não custa o guia pelo resto da
visita.

**Nenhum dos dois conta nada para a API.** O fechar que persiste de verdade é
outro, e está logo abaixo.

**O título do card é o próprio toggle** — é onde a mão vai antes do chevron.

**A linha do card faz o passo**, não abre o guia. O card existe para nomear a única
coisa que falta; uma linha que nomeia e depois responde com uma lista está pedindo
que a pessoa procure ali o que acabou de ler. Com destino ela é um link de verdade
(clique do meio, copiar endereço); sem destino e sem `onStepAction`, aí sim ela
abre o guia, que é o que sobra a oferecer.

O fechar que **persiste** mora no menu de objetivos, atrás do `Editar` — "fechar tudo
por um tempo". Ele dispensa cada lista do guia, cada uma pelo próprio `snooze_days`, e
elas voltam sozinhas. Está ali e não no cabeçalho porque é um tipo de ato diferente
dos dois ao lado, e porque aquele menu já é a tela sobre o que a pessoa quer no guia
dela. Do seu código é o `userkit.dismissChecklist(id)`, uma lista por vez.

## O que satisfaz um passo

Três tipos, e a diferença entre os dois de evento é **quem publica**.

| Tipo            | Satisfeito por                                                                                  | Garantia                 |
| --------------- | ----------------------------------------------------------------------------------------------- | ------------------------ |
| `event`         | Um fato que o UserKit publica sobre um contato — `contact.identified`, `customer_member.joined` | O outbox: at-least-once  |
| `product_event` | Um evento que o **seu** produto manda pelo `track()`                                            | Mais fraca — veja abaixo |
| `entitlement`   | Um recurso que o time do contato tem, decidido a cada leitura                                   | Nada é gravado           |

## Da pessoa ou do time

Todo passo tem um `scope`, e o padrão é `contact`: uma pessoa, um passo. É o que
todo passo significava antes deste campo existir.

`scope: "customer"` é para o trabalho que é **da conta**, não de ninguém em
particular — conectar a plataforma de pagamento, importar um catálogo, configurar
um domínio. Ele fica satisfeito para todo mundo do time assim que **qualquer**
membro faz:

```json theme={null}
{ "key": "cobranca", "title": "Conectar a plataforma de vendas",
  "kind": "product_event", "event_type": "billing.connected",
  "scope": "customer" }
```

Sem isso, o segundo sócio de um clube vê "conectar a plataforma" em cinza — sendo
cobrado, por nós, de um trabalho que o colega já terminou.

**Nada é gravado para o time.** A linha de progresso continua sendo da pessoa que
fez; isso é a mesma linha respondendo uma pergunta diferente, na leitura, dentro
de uma requisição que já resolveu de qual time se trata — que é exatamente o
arranjo do `entitlement`, um tipo ao lado. É por isso que o argumento contra
espalhar um fato por um quadro inteiro continua de pé: nada é espalhado.

Duas consequências que valem saber:

* **O funil conta TIMES** num passo de escopo de time, e pessoas nos outros. O
  `scope` volta junto do número na leitura de conclusão, porque 40 e 40 em unidades
  diferentes é uma tabela onde duas respostas parecem concordar.
* **A lista inteira passa a ter `completed: null`**, como já acontece com um passo
  `entitlement` — e pela mesma razão. Quem escreve a marca de "terminou a lista" é
  um consumidor, que não tem requisição e portanto não tem time; materializar uma
  significaria escolher um time pela pessoa e estar errado em todos os outros de
  que ela participa. O passo continua com o número dele; a lista responde "não dá
  para dizer" em vez de contradizer o widget.
* **Sem time resolvido, cai para a própria pessoa.** Um contato sem time não é um
  contato cujo time não fez nada — não há time. É também o estado de todo mundo
  antes de [espelhar as contas](/pt-br/api-reference/customer-teams/create-or-mirror-a-team).

`scope` é congelado depois de criado, como `kind`: virá-lo com gente segurando
progresso mudaria o que as linhas delas significam. Num passo `entitlement` ele é
recusado — esse tipo já é uma pergunta sobre o time por construção.

**O passo `product_event` é como se expressa aquilo de que um onboarding
realmente é feito** — "criou o primeiro projeto", "publicou algo", "convidou
alguém pelo seu próprio fluxo". Você manda o evento que já manda:

```ts theme={null}
userkit.track("project.created", { id: "prj_1" });
```

Nada nessa chamada menciona checklist. Se existir um passo ativo esperando esse
nome naquele ambiente, o evento é encaminhado para o nosso barramento e o passo é
satisfeito para aquela pessoa; se não existir, custa uma consulta a um mapa e
segue sendo um evento de analytics comum.

<Note>
  **A garantia dele é mais fraca que a dos outros dois, e vale saber qual.** O
  analytics descarta eventos quando o buffer enche, sob carga; o nosso barramento
  não. Depois que o evento é gravado, o resto do caminho é do barramento e a
  garantia é a mesma de todo o resto — mas o começo desse caminho pode perder um, e
  um evento perdido é um passo que fica cinza.
</Note>

**Um nome não pode ser recusado por não existir.** O seu vocabulário de eventos é
seu e é aberto, então a API confere o formato e mais nada — o que também quer
dizer que dá para escrever o guia antes de instrumentar o produto, nessa ordem. O
que o painel faz no lugar é dizer o que sabe: *visto 1.284×, por último hoje*, ou
**este ambiente nunca recebeu esse evento**. Um zero ali é quase sempre erro de
digitação, e é o único aviso que vai existir.

Visitantes anônimos ficam de fora: o progresso é de um contato identificado, e o
widget cunha um visitante por carregamento de página.

**E o fato pode nascer no seu backend.** Metade do que um onboarding mede não
acontece num browser — um pagamento confirmado por webhook, um contrato
assinado, uma exportação que um worker terminou. Esses vão por
[`POST /v1/track`](/pt-br/api-reference/analytics/track-events-from-your-backend)
com a chave secreta do ambiente, e cada linha **nomeia o contato** a que
pertence:

```bash theme={null}
curl -X POST $API/v1/track \
  -H "Authorization: Bearer uk_sk_live_…" \
  -H "Idempotency-Key: nightly-2026-08-02" \
  -d '{"events":[{"id":"0197a3a6-…","name":"invoice.paid",
       "external_id":"usr_42","occurred_at":"2026-08-02T14:07:31Z"}]}'
```

É a mesma máquina daqui em diante: o nome bate com o passo, o passo é satisfeito
para aquela pessoa. A diferença é só quem manda. `POST /v1/events` — a porta que
o `track()` do browser usa — **não consegue nomear ninguém**, e um evento sem
contato não satisfaz passo de ninguém: se o seu passo depende de um fato do
servidor, é esta a porta.

## Para onde um passo leva

Um guia que nomeia o trabalho e esconde a porta faz o leitor navegar duas vezes.
Então um passo carrega o próprio destino:

```json theme={null}
{
  "key": "domain",
  "title": "Conecte seu domínio",
  "kind": "product_event",
  "event_type": "domain.connected",
  "action_url": "/settings/domain"
}
```

O `action_url` aceita um **caminho do seu próprio app** (`/settings/domain`) ou
uma URL `http(s)` absoluta — o mesmo par que uma
[notificação](/pt-br/guides/notifications) e um banner aceitam. A forma
relativa a protocolo é recusada: `//evil.example/x` começa com barra, passa por
caminho em qualquer revisão, e é uma URL absoluta para o host de outra pessoa.

O widget desenha esse passo como um **link de verdade**, não como um botão que
navega — clique do meio, copiar endereço e abrir em outra aba são a razão de um
link ser um link. A linha inteira é o alvo e uma seta diz isso; uma URL absoluta
abre em nova aba, um caminho fica no app.

O `action_label` **não é desenhado na linha**. Uma palavra à direita de toda
linha clicável é uma coluna de verbos competindo com os títulos, e a seta já diz
que a linha abre. O que ele ainda faz é nomear a ação para um leitor de tela, e
declarar que um passo **sem** destino tem uma — aquela que o seu próprio código
trata.

**Um passo concluído continua com a porta aberta.** Feito não é sumido: quem
conectou o domínio semana passada ainda quer aquela tela.

### A frase que não cabe na linha

Um passo é uma linha, e isso é o ponto — um guia que se lê de relance é um guia que
as pessoas leem. Mas alguns passos precisam de uma segunda frase: por que este está
travado, o que a coisa é de fato, o que ela comprou. Essa frase vai no hover, depois
de um segundo:

```json theme={null}
{
  "title": "Conectar seu domínio",
  "hint_pending": "Fale sobre a sua empresa para liberar esta tarefa.",
  "hint_done": "Suas cobranças agora saem no seu domínio."
}
```

**Dois textos, e o par é a funcionalidade.** A mesma linha quer dizer coisas
diferentes antes e depois. Pendente, a frase útil é um motivo ou uma instrução;
concluído, uma instrução é ruído e o útil é o que aquilo comprou. Um campo só
obrigaria você a escrever uma frase errada na metade das vezes — e uma frase errada
na metade das vezes é uma frase que ninguém escreve.

Os dois são opcionais e independentes. Só o `hint_pending` é o caso comum e lê
certo: nada no hover depois de feito, que é o tanto honesto a dizer sobre uma coisa
que a pessoa já fez.

Uma **checklist** aceita um terceiro, o `hint_locked`, porque um título tem um estado
que uma linha não tem. O widget já desenha um cadeado e o nome do que o grupo espera,
e um nome é uma porta, não um motivo:

| Estado    | Campo          | O que diz                                           |
| --------- | -------------- | --------------------------------------------------- |
| Pendente  | `hint_pending` | Por que importa, ou o que fazer                     |
| Travada   | `hint_locked`  | Por que a porta existe — na linha fica só o cadeado |
| Concluída | `hint_done`    | O que aquilo comprou — ou nada                      |

Um **passo** tem só dois, e a ausência é deliberada: um passo não tem estado travado.
O que parece passo travado em outros produtos é passo dentro de um grupo travado, e um
grupo travado é desenhado fechado — então um terceiro campo ali seria configuração que
nunca consegue aparecer. O caso mais próximo é um passo de entitlement de um recurso
que o time não tem, e esse é honestamente *pendente*: "disponível no Pro" é
`hint_pending`.

No máximo 200 caracteres. Mais que isso é um parágrafo no hover, que some no instante
em que a pessoa move o mouse para ler.

### Ele fica verde enquanto a pessoa está olhando

Um passo espera um fato chegar no nosso barramento, o que leva alguns segundos
depois do `track()`. O widget não faz polling — um poll correria com o dreno em
vez de ler um número vivo — então, sem ajuda, o check só apareceria no próximo
carregamento, muito depois do momento em que ele significava alguma coisa.

Por isso o `track()` também avisa **localmente**:

```ts theme={null}
userkit.track("content.published");
// o passo que espera esse nome fica verde agora; o fato confirma em alguns
// segundos
```

Nada disso é escrita. O evento viaja do jeito de sempre, a API grava, e quem
satisfaz o passo continua sendo o barramento — isto só conta para o que está na
tela que alguém fez a coisa, segundos antes da verdade chegar. O guia relê duas
vezes para trocar o palpite pela resposta, e **se o fato não chegar, o check é
retirado**: um evento pode ser descartado sob carga, recusado por formato, ou ser
de um visitante para quem nada grava progresso — e um check que ficasse seria o
widget mentindo pelo resto da sessão.

A marca otimista também não decide nada. O `completed` continua sendo a resposta
do servidor, então nenhuma lista parabeniza ninguém, se esconde, nem grava
conclusão em cima de um palpite.

Para o app que termina algo sem chamar `track()`:

```ts theme={null}
userkit.signalChecklist();   // não afirma nada; só pede de novo
```

### Quando a ação não é uma navegação

Metade do que um passo quer fazer no clique é abrir a sua própria modal, começar
um tour, ou focar um campo na página em que a pessoa já está. Nada disso tem
endereço, então não vive no banco — o seu código é que vive:

```tsx theme={null}
<OnboardingChecklist
  onStepAction={(step) => {
    if (step.key === "invite") {
      abrirModalDeConvite();
      return true; // tratado — não siga o link
    }
    // qualquer outro: deixa o action_url do passo acontecer
  }}
/>
```

O handler roda **antes** do link, e devolver `true` cancela a navegação. É isso
que permite um único handler interceptar os dois passos que o seu app conhece e
deixar o resto navegar.

Um passo cuja ação é só uma função tem `action_label` e nenhum `action_url` — ele
declara que existe uma porta e que a sua página é quem abre. Numa página que
nunca ligou o `onStepAction`, esse passo desenha um controle que não faz nada, e
é por isso que o painel avisa ao lado do campo.

<Note>
  **Clicar nunca satisfaz um passo**, em nenhuma das duas formas. Se o clique *é* o
  que você está medindo, a composição é um passo `product_event` cujo `track()`
  está no mesmo handler:

  ```tsx theme={null}
  onStepAction={(step) => {
    if (step.key !== "tour") return;
    comecarTour();
    userkit.track("tour.started");   // é isto que deixa o passo verde
    return true;
  }}
  ```
</Note>

## Várias listas, um guia

Cada checklist em que a pessoa está é desenhada como um **grupo** de um guia só: o
primeiro não concluído e desbloqueado fica aberto, os outros dobrados. Um guia que
abrisse tudo seria uma parede de vinte passos, e a pergunta de quem chega é "e agora",
não "o que existe".

**Ordene com `unlocks_after`.** Uma lista que espera outra é desenhada fechada, com um
cadeado e o nome do que ela espera — ela continua NO guia em vez de sumir, porque ver o
que vem a seguir é a diferença entre um guia e uma fila. Só no mesmo ambiente, e um
ciclo é `400`: duas listas esperando uma pela outra é um par que nunca abriria.

**Deixe a pessoa escolher com `selectable`.** Uma lista selecionável fica fora do guia
até ela colocá-la lá, pelo modal "do que você precisa para começar?" atrás do
**Editar** do guia. O público continua decidindo o que é *oferecido*; isto entrega o
último passo a ela. O menu viaja junto com o guia numa requisição só, então o modal
abre com as escolhas dela já marcadas.

Salvar envia a **resposta inteira**, nunca um diff — senão "desmarquei isto" e "isto
foi lançado enquanto meu modal estava aberto" seriam a mesma requisição, e a segunda
desfaria o seu próprio lançamento sem avisar.

Nada disso mexe no que satisfaz um passo. Escolher decide quais listas estão no guia;
um passo continua sendo satisfeito por um fato que chega no bus ou por um direito do
plano, e nenhuma tela em lugar nenhum marca um.

Inline é o lugar de um dashboard, num espaço que você abriu para ele. As posições
flutuantes são para o produto que não tem esse espaço: um cartão no canto que a pessoa
olha entre uma tarefa e outra. Essas trazem um fechar, que o inline não precisa — um
painel sobre a página do qual não se consegue livrar é um painel que as pessoas
aprendem a contornar.

**Fechar é por checklist**, via `POST /v1/contact/checklist/{id}/dismiss`, que o
`<OnboardingChecklist />` chama por você. Um ambiente tem várias listas ativas, então
uma marca na pessoa faria a que você lançar no mês que vem nascer escondida para todo
mundo que fechou a anterior — dispensar uma não diz nada sobre as outras, nem sobre as
que ainda não existem.

**E é um adiamento, não uma resposta.** O `snooze_days` da checklist decide quanto
tempo: sete dias por padrão, e depois o cartão volta. Quem aperta o X num guia de setup
quase nunca quer dizer "nunca mais me ajude" — quer dizer "agora não", e gravar a
leitura mais forte possível do gesto mais fraco possível é como um guia some da conta
de alguém que nunca terminou de configurar nada.

Use `snooze_days: 0` para a checklist que é mesmo uma oferta única; aí o fechar vale
para sempre. Entre os dois, períodos maiores são para a lista que a pessoa pode
legitimamente não querer por um tempo — quem nunca vai comprar o recurso de um passo de
entitlement veria o mesmo cartão voltar pelo resto da vida da conta, e uma cobrança com
agenda continua sendo uma cobrança.

Mudar o número nunca mexe num adiamento em curso. O fim é calculado quando a pessoa
fecha o cartão e fica gravado, então uma promessa feita a alguém não é encurtada por
uma edição que essa pessoa nunca viu.

**A página dedicada de setup pede as fechadas.** Fechar diz "no meu dashboard não", e
quem navega até `/getting-started` acabou de dizer o contrário sobre aquela tela:

```tsx theme={null}
<OnboardingChecklist alwaysShow />
```

Isso lê com `?include_dismissed=true`, e cada checklist volta carregando `dismissed`,
para uma página que desenha a própria UI poder dizer isso. **O botão de fechar sai
junto** — um controle que esconde algo que volta no render seguinte é um controle que
não funciona.

Ele passa por cima da dispensa e de mais nada: uma checklist desligada continua
desligada, o público continua decidindo, e uma escolhível ainda precisa ter sido
escolhida. Essas são coisas sobre a checklist; fechar é uma preferência sobre uma tela.

**Escolher a lista de novo no menu de objetivos traz ela de volta na hora**, sobrando o
que sobrar do adiamento. Aquela tela é onde alguém diz "quero isto no meu guia", então
uma dispensa que sobrevivesse a ela seria uma recusa que ninguém falou em voz alta.

Não é uma recusa e não mexe no progresso: fechar diz "aqui não", e os passos continuam
sendo satisfeitos por fatos que chegam no bus. O painel lê dois números ao lado do funil
de conclusão — `dismissals`, quantas pessoas já fecharam, e `hidden`, para quantas está
escondida agora. A diferença entre os dois é o adiamento funcionando. Os dois **iguais**
é a cara de um cartão que as pessoas estão empurrando para longe.

Num app Next, importe do `@userkit/nextjs` — é o mesmo componente, e em
[modo proxy](/pt-br/customer-auth/session-tokens) a leitura é encaminhada pelos
handlers, com a sessão num cookie httpOnly da sua própria origem. Só a leitura: não há
metade de escrita em lugar nenhum desta superfície.

```json theme={null}
{
  "checklists": [
    {
      "id": "…",
      "name": "Primeiros passos",
      "completed": false,
      "steps": [
        { "key": "verify", "title": "Verifique seu e-mail", "kind": "event", "position": 1, "satisfied": true, "satisfied_at": "2026-07-30T10:12:00Z" },
        { "key": "invite", "title": "Convide alguém do time", "kind": "event", "position": 2, "satisfied": false, "satisfied_at": null }
      ]
    }
  ]
}
```

Uma **visitante anônima recebe `200` e uma lista vazia**, não um `401`. O widget roda
na sua landing page, e um erro de console é a forma errada de dizer "não há nada para
mostrar aqui".

`satisfied_at` é sempre `null` num passo de entitlement: nada foi armazenado, então
não há momento a relatar. `completed` é derivado dos passos em vez de guardado ao
lado deles, o que é o que o mantém correto também numa lista que carrega um passo de
entitlement.

O id do cliente viaja com a leitura, porque é ele que decide os passos de
entitlement. Ele identifica e nunca autoriza — um time a que a sessão não pertence
não casa com vínculo nenhum.

## Quem vê

`segment_id` mira a checklist numa [audiência](/pt-br/guides/segments), ou `null`
para todo mundo. Ele decide **quem vê a lista**, não quem é registrado contra ela: o
progresso é escrito para quem produziu o fato, então alguém que entra no segmento
depois chega com os passos já cumpridos já marcados.

## O funil

```http theme={null}
GET /v1/organization/checklists/{id}/completion
```

```json theme={null}
{
  "audience": 812,
  "completed": null,
  "steps": [
    { "key": "verify", "kind": "event", "satisfied": 640, "materialized": true },
    { "key": "domain", "kind": "entitlement", "satisfied": null, "materialized": false }
  ]
}
```

**Um passo de entitlement reporta `null`, não um zero que ele não ganhou.** Nada é
armazenado para esses passos, então não há contagem a dar — e `0` seria um número
sobre o qual alguém agiria. `materialized` diz qual dos dois você está olhando, para
que um gráfico desenhe uma lacuna em vez de uma barra.

`completed` é `null` pelo mesmo motivo sempre que a checklist carrega um passo de
entitlement: um número de "terminou a lista inteira" calculado sobre os passos que
ele enxerga seria um piso apresentado como total. Uma tranquilização é exatamente o
que faz alguém parar de conferir.

`audience` é quem está no segmento, ou todo contato identificado do ambiente quando a
checklist é para todo mundo.

## O que ela publica

`checklist.step_completed` e `checklist.completed` são entregáveis como
[webhooks](/pt-br/guides/webhooks) — o segundo é o que vale assinar, porque "essa
pessoa terminou o onboarding" é o momento de que uma sequência de boas-vindas ou um
alerta de vendas pendura.

`checklist.completed` dispara **no máximo uma vez por pessoa por checklist**,
sustentado por uma linha marcadora escrita na mesma transação. O barramento é
at-least-once, então um consumidor que rodasse de novo sem ela anunciaria uma
formatura duas vezes.

Ele dispara apenas para uma checklist feita **inteiramente de passos de evento**. Um
passo de entitlement é decidido na leitura, contra um time, então o caminho que
anuncia a conclusão — que roda quando um fato chega, e conhece só a pessoa — não
consegue dizer se a lista terminou. Uma checklist da qual você quer pendurar uma
automação deve ser feita só de passos de evento.

Os payloads levam ids, chaves e títulos: o passo que alguém satisfez e a lista a que
ele pertence. Nunca a regra por trás — qual fato ou qual funcionalidade um passo
espera é configuração sua, e quem assina não precisa disso para agir sobre alguém ter
chegado lá. Editar ou excluir uma checklist não é entregue.

## Permissões

Toda a superfície de staff é `engagement:manage` — owner e admin por padrão. Um nome
cobre checklists, changelog e pesquisas: é o mesmo ato, que é operar o que seus
usuários veem dentro do seu produto.
