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

# Banners

> Um recado na tela de quem usa o seu produto, num lugar que você abriu — mirado num público, limitado por uma janela, e um por slot de cada vez.

Um banner é o que você diz para alguém dentro do seu produto, onde a pessoa já
está: o aviso de manutenção, o trial que acaba em três dias, o prazo da migração, a
faixa da promoção. É a coisa mais curta desta API — um título, às vezes um botão —
e quase todo o desenho dela é sobre onde aparece e quando para.

## Você escolhe o lugar, a gente preenche

Não existe barra flutuante. Nada é injetado no seu layout, e não há opção que faça
isso. Você renderiza o componente onde a sua página tem espaço:

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

<Banner slot="dashboard-top" />;
```

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

O banner declara o mesmo `slot` no painel, e os dois se encontram por string
exata. Letras minúsculas, números, hífen e underscore — um nome com maiúscula ou
espaço é **recusado em vez de normalizado**, porque essa string é escrita duas
vezes em dois lugares diferentes e maiúscula é como isso dá errado sem ninguém ver.

O motivo não é gosto. Um widget que colocasse uma faixa fixa na viewport cobriria o
cabeçalho de todo produto que já tem um, em produção, no instante em que alguém
salvasse uma linha num painel. Um slot é um lugar que o dono da página declarou ter
espaço, e é o único tipo de lugar em que um terceiro pode desenhar.

Nada verifica se o seu app ainda renderiza um slot — não vemos o seu código, e um
aviso baseado em palpite seria pior que aviso nenhum. O painel oferece os slots que
os seus próprios banners já usam.

## Um banner por slot

Um slot mostra um banner de cada vez. Quando há mais de um candidato, ganha o de
maior `priority`, e empate vai para o mais novo.

Isso é decidido **no banco**, na mesma instrução que decide todo o resto, então a
resposta é idêntica para o componente React, para um fetch de um app mobile e para
a prévia do painel. Um cliente que escolhesse o próprio vencedor seria uma segunda
opinião que ninguém pediu.

## Dois tipos

`text` é um título, um texto embaixo, um ícone opcional e um botão opcional. O
widget desenha com a sua paleta — suas cores, seu raio, sua fonte — para parecer
parte do produto e não algo alugado.

`image` é uma arte sua, opcionalmente com link. Não tem título por construção: uma
chamada por cima de uma imagem desenhada com a própria chamada são duas chamadas.
O arquivo é hospedado por você; a API recebe uma URL `http(s)` absoluta e não
guarda mais nada.

**Para onde qualquer um dos dois leva** é o `cta_url` — o destino do botão no
banner de texto, o link da figura inteira no de imagem. Ele aceita um caminho
começando em `/` (`/planos`) ou um endereço `http(s)` completo, as mesmas duas
formas que uma [notificação no produto](/pt-br/guides/notifications) aceita — e o
caminho é, se tanto, a metade mais natural aqui: um banner é desenhado dentro do
seu próprio produto. Um esquema que executa é recusado, e a forma relativa a
protocolo `//host/caminho` também: ela se lê como caminho e nomeia outro host. O
`image_url` é o que continua absoluto — um `<img src>` relativo resolveria contra
qualquer página em que o widget for desenhado.

O tipo **não muda depois de criado**. Os dois preenchem colunas diferentes, e o
banco recusa uma linha que carregue as duas — trocar apagaria o que alguém
escreveu, sem volta. Crie um banner do outro tipo, o que também mantém a conta de
dispensas presa à coisa que as pessoas realmente viram.

`body` é texto puro, não markdown. Um banner que precisa de formatação é um
[post de changelog](/pt-br/guides/changelog) com um botão apontando para ele.

## Quando aparece

Três coisas decidem, e elas respondem perguntas diferentes:

| Campo       | Responde                               |
| ----------- | -------------------------------------- |
| `active`    | Está pronto e aprovado                 |
| `starts_at` | A partir de quando vale (`null` — já)  |
| `ends_at`   | Até quando (`null` — não para sozinho) |

`active` nasce **false**: um banner existe enquanto o texto está sendo escrito, e
criar um nunca coloca nada na frente de ninguém. É também o botão de desligar, e é
por ele que se tira do ar uma faixa da qual você se arrependeu e que já está na
tela de alguém — vale na próxima leitura do widget, não numa expiração de cache.

`ends_at` é o que tira a faixa da promoção do ar na segunda sem ninguém lembrar. É
o campo mais valioso daqui e o que mais fica vazio.

Os dois limites são RFC 3339 com fuso, comparados com o relógio da própria API. Uma
data seca seria um instante no fuso que o leitor imaginou, então "a promoção acaba
à meia-noite" encerraria na sexta para uns clientes e no sábado para outros.

## Quem vê

`segment_id` é para quem ele é, ou `null` para todo mundo — o mesmo
[segmento](/pt-br/guides/segments) que as suas feature flags, pesquisas e posts de
changelog usam, nunca um filtro próprio.

Visitante **anônimo** não é um caso especial: ele tem um contato como qualquer
outro, então vê os banners mirados em todo mundo. Isso é metade do motivo deste
módulo existir — o aviso de manutenção na tela de entrada do seu produto.

O público nunca viaja até o leitor. "Este banner é mirado num segmento" é a sua
própria análise dos seus usuários, e quem está olhando não é para quem essa
informação é.

## Dispensar

```ts theme={null}
const banners = await userkit.listBanners();
await userkit.dismissBanner(id);
```

```json theme={null}
{
  "banners": [
    {
      "id": "…",
      "slot": "dashboard-top",
      "kind": "text",
      "dismissible": true,
      "title": "Manutenção programada no domingo",
      "body": "A API fica somente leitura entre 02:00 e 04:00 (UTC).",
      "icon": "wrench",
      "tone": "warning",
      "cta_label": "",
      "cta_url": ""
    }
  ]
}
```

Todos os slots voltam numa chamada só, então uma página com uma faixa no topo e um
aviso na cobrança custa uma ida e volta. **Ramifique pelo `kind`**, nunca por um
campo estar vazio: só os campos do tipo viajam, e é isso que mantém "uma imagem sem
descrição" distinguível de "isto não é uma imagem".

A dispensa pertence à pessoa e a acompanha em todos os aparelhos. É idempotente, e
dispensar um banner que não existe, que é de outra organização ou que já estava
dispensado responde o mesmo `204` — um `404` aqui diria a quem tem a chave
publicável se um uuid existe.

`dismissible: false` é para os avisos que são **estado do produto** e não notícia:
"esta conta fica somente leitura até você confirmar o e-mail". Esconder isso
esconderia a explicação do que a pessoa está prestes a encontrar. Dispensar
continua sendo registrado e depois ignorado, então nenhum cliente precisa conhecer
a regra para evitar um erro — e um banner que você tornar dispensável depois já
está fechado para quem tentou.

Religar um banner não o mostra de novo para quem já fechou. Dizer a mesma coisa de
novo é um banner novo, o que também é o que faz "quantas pessoas fecharam este
aqui" ser um número sobre um aviso só.

## Alcance

A lista mostra quatro números ao lado de cada banner, dos **últimos 30 dias**:

|            | O que conta                                                  |
| ---------- | ------------------------------------------------------------ |
| `views`    | Exibições.                                                   |
| `viewers`  | As pessoas por trás delas — é este que se cita como alcance. |
| `clicks`   | Cliques no botão.                                            |
| `clickers` | As pessoas por trás deles.                                   |

Um painel que alguém abre onze vezes por dia é onze exibições e um leitor, e o par é
justamente o ponto: citar o primeiro como alcance é como um banner parece um sucesso
para ninguém. `viewers` conta todo contato distinto, visitante anônimo incluído,
porque o banner é desenhado para ele também — deixar de fora quem ainda não entrou
seria deixar de fora exatamente o público a que um aviso costuma se dirigir.

**Uma exibição é a faixa CRUZAR a dobra**, não o componente renderizar. Um banner
abaixo da dobra é desenhado a cada carga de página e visto por ninguém, então o
`<Banner />` reporta a partir de um `IntersectionObserver` a 50% de visibilidade, uma
vez por banner por carga de página — por mais vezes que uma SPA remonte o componente.

Os números são `null`, nunca `0`, quando o analytics não pôde ser lido: zero diria
que o banner não alcançou ninguém. O `engagement.available` da resposta diz qual dos
dois é.

Isso são **os seus próprios eventos de analytics**: `banner.shown` e `banner.clicked`,
escritos pelo widget pela mesma porta que os seus `track()` usam, carregando
`banner_id`, `slot` e `kind` — e nenhum texto. Eles aparecem
[no explorador](/pt-br/guides/analytics) como qualquer outro evento, que é onde mora
o histórico sem limite: os 30 dias do painel são uma janela, e um banner que rodou uma
semana em março ficaria com cara de fracasso conforme os zeros de hoje se acumulassem.

Não existe tabela de impressões do nosso lado, e isso é deliberado: um contador
gravado a cada carga de página pertence ao lugar onde todo contador de carga de
página já vai, com o lote e a política de abuso que aquele endpoint já tem. O clique é
contado ao lado do seu link e não por um redirecionamento nosso — passar todos os seus
botões por esta API faria eles pararem de funcionar quando a gente parasse. A troca é
dita e não escondida: um clique que navega antes da fila esvaziar é um clique que
ninguém contou, o que é um número um pouco baixo em vez do seu produto quebrado
durante um incidente nosso.

## O que não existe aqui

**E-mail.** Um banner é desenhado dentro do seu produto e em nenhum outro lugar. O
que se envia é [campanha](/pt-br/guides/campaigns).

## Permissões

`engagement:manage` — owner e admin por padrão, a mesma do onboarding, do changelog
e das pesquisas. Leitura incluída: o que você diz para os seus próprios usuários é
cópia sua, e nenhuma permissão mais larga mostra isso.
