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

# Tracking de cliques

> Como cliques são registrados, atribuídos e protegidos (LGPD).

O Tracking é a porta de entrada do funil de afiliados: cada hit em um Link de afiliado vira um Clique imutável (`clk_` + ULID), com o visitante identificado, a geolocalização derivada do IP e os dados que o módulo de [Atribuição](/docs/conceitos/atribuicao) usa para ligar uma venda a um afiliado. O objetivo é duplo: **medir** o tráfego de cada afiliado (separando bots de humanos) e **garantir atribuição confiável** mesmo quando o clique e a [conversão](/docs/conceitos/conversoes-e-fraude) acontecem em momentos ou dispositivos diferentes.

A estratégia é **server-side first**: o id do clique é anexado à URL de destino como `repass_cid`, para o seu sistema capturá-lo no signup e devolvê-lo na conversão via API. Cookie first-party, e-mail hasheado e fingerprint são *fallbacks*, nessa ordem.

<Note>
  As rotas de ingestão `GET /t/{token}`, `POST /track/click` e `POST /track/identify` são **públicas, não exigem `Authorization`**. O tenant (organização) é derivado do próprio Link, nunca de sessão. Essas rotas têm CORS liberado (GET/POST) e estão fora do mecanismo de `Idempotency-Key`. As rotas de consulta (`/clicks`, `/links/{id}/stats`) exigem sessão ou API key e são sempre escopadas à sua organização.
</Note>

## O fluxo do redirect `/t/{token}`

O redirect é o caminho mais comum: o afiliado divulga uma URL como `https://api.userepass.com/t/ana-promo`, o visitante clica, o clique é registrado server-side e ele segue para o destino com o `repass_cid` anexado.

```mermaid theme={null}
sequenceDiagram
    participant V as Visitante
    participant API as API (rota pública /t/)
    participant Geo as GeoIP
    participant S as Seu site

    V->>API: GET /t/ana-promo?sub=campanha-x (cookie repass_vid?)
    API->>API: resolve Link → Programa → Afiliado (valida status)
    alt programa archived ou afiliado pending
        API-->>V: 302 landing/destino (+ repass_notice se archived), sem registrar
    else link inactive / afiliado rejected|banned
        API-->>V: 410 { error.code: "gone" }
    else clique válido
        API->>API: detecta bot + calcula fingerprint SHA-256
        API->>Geo: lookup(ip), falha vira geo null
        API->>API: registra o clique e emite click.recorded
        API-->>V: 302 destino?repass_cid=clk_... + Set-Cookie repass_vid
        V->>S: navega; seu site captura repass_cid p/ a conversão (S2S)
    end
```

Em caso de sucesso, a resposta é um **302** para o destino com `repass_cid=clk_...` anexado (preservando a query existente) e um `Set-Cookie repass_vid` (httpOnly, sameSite=lax, secure em produção, `maxAge` = janela de atribuição em segundos).

```bash theme={null}
curl -i "https://api.userepass.com/t/ana-promo?sub=campanha-x"
# HTTP/1.1 302 Found
# location: https://loja.exemplo.com/?repass_cid=clk_01J9Z8K3M0XQ2R7B4D6F8H0A1C
# set-cookie: repass_vid=vis_01J9Z8K3M0XQ2R7B4D6F8H0A1C; Max-Age=2592000; Path=/; HttpOnly; SameSite=Lax
```

O `token` na rota aceita 1 a 64 caracteres (mais permissivo que o formato de criação do Link, 3 a 50 `[a-z0-9-]`), de propósito: um token malformado responde **404** (e não 400). O lookup é case-insensitive.

### Como o destino é resolvido

A URL de destino segue a cadeia `destinationUrl` do Link → `landingUrl` do Programa → nenhum destino. Quando há destino, é o redirect 302; quando não há nenhum, o redirect responde 404 (mesmo tendo registrado o clique).

A query `sub` (1 a 120 chars) sobrepõe o `subId` default do Link, permitindo sub-tracking de campanha por hit. Veja também [Links e cupons](/docs/conceitos/links-e-cupons).

### Status que alteram o fluxo

<Accordion title="Programa archived, afiliado pending, link inactive...">
  | Situação                          | Comportamento                                                                                                                  |
  | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
  | Programa `archived` (com landing) | **302** para a landing com `repass_notice=program_archived`, **sem** registrar o clique, **sem** cookie e **sem** `repass_cid` |
  | Programa `archived` (sem landing) | **410** `error.code: "gone"`                                                                                                   |
  | Afiliado `pending`                | Redireciona ao destino **sem registrar**; sem destino algum → **410** `error.code: "gone"`                                     |
  | Afiliado `rejected` / `banned`    | **410** `error.code: "gone"`                                                                                                   |
  | Link `inactive`                   | **410** `error.code: "gone"`                                                                                                   |
  | Token inexistente                 | **404** `error.code: "resource_not_found"`                                                                                     |

  Todas as respostas **410** usam o mesmo `error.code` `gone` (apenas a `message` varia entre "link inativo" e "programa arquivado"). Programa `paused` e afiliado `paused` registram cliques normalmente.
</Accordion>

## Identidade do visitante

Cada clique grava como o visitante foi identificado. Há três sinais, persistidos no próprio Clique:

<ResponseField name="visitorId" type="string (vis_ + ULID)">
  Identificador do visitante, persistido no cookie first-party `repass_vid` com `maxAge` igual à janela de atribuição. Um `visitorId` recebido (cookie ou payload) só é reaproveitado se for um id válido; caso contrário um novo é gerado silenciosamente: formato inválido nunca é erro.
</ResponseField>

<ResponseField name="emailHash" type="string (SHA-256 hex)">
  Associação cross-device. O e-mail **nunca** trafega cru: apenas o SHA-256 em hex (64 chars). Veja [`/track/identify`](#cross-device-via-e-mail-hasheado) abaixo.
</ResponseField>

<ResponseField name="fingerprint" type="string (SHA-256 hex)">
  Fallback probabilístico de identidade: SHA-256 de `ip | user-agent | accept-language`, calculado server-side. Por design tem baixa entropia (colide em NAT/CGNAT) e é o **último** sinal na ordem de matching da [atribuição](/docs/conceitos/atribuicao).
</ResponseField>

## Atribuição server-side (S2S)

A atribuição primária não depende de cookie nem de fingerprint: ela depende do **id do clique**. No redirect, o `repass_cid=clk_...` é anexado à URL de destino. Seu sistema deve capturá-lo no signup (por exemplo, persistindo-o junto ao usuário) e devolvê-lo no evento de conversão. Esse é o sinal mais forte e o que você deve priorizar.

```bash theme={null}
# Mais tarde, ao registrar a venda, devolva o clique capturado:
curl https://api.userepass.com/conversions \
  -H "Authorization: Bearer rstr_..." \
  -H "Content-Type: application/json" \
  -d '{
    "clickId": "clk_01J9Z8K3M0XQ2R7B4D6F8H0A1C",
    "externalId": "order_9281",
    "amount": 19900
  }'
```

Quando não há `clickId`, a conversão cai para os fallbacks na ordem `visitorId → emailHash → fingerprint`. Os detalhes do algoritmo de candidatos e dos modelos (last-click é o default do Programa) estão em [Atribuição](/docs/conceitos/atribuicao) e [Conversões e fraude](/docs/conceitos/conversoes-e-fraude).

<Tip>
  Valores monetários são sempre inteiros em centavos (`19900` = R\$ 199,00). Veja [Convenções de IDs e recursos](/docs/convencoes/ids-e-recursos).
</Tip>

## Geolocalização

A cada clique, o IP é resolvido para `country` (ISO), `region` (ISO da primeira subdivisão) e `city` (nome em inglês). A resolução é **best-effort**: qualquer falha deixa a geo `null` e **nunca** impede o registro do clique. A geo derivada é preservada mesmo depois que o IP é truncado pela minimização de dados (LGPD).

## Detecção de bot

Bots são **registrados** (com `bot: true` e um `botReason`), porém ficam fora dos stats públicos e da atribuição, auditáveis e filtráveis via `GET /clicks?bot=true`.

| `botReason`    | Critério                                                                                                                                                                                                                                                    |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `user_agent`   | User-agent ausente/vazio (nenhum browser real omite o UA), ou que casa com a lista de padrões: crawlers, headless, `curl`, `wget`, `python-`, `scrapy`, `facebookexternalhit`, `whatsapp`, `telegrambot`, `pingdom`, `lighthouse`, `uptime`, `monitor` etc. |
| `ip_blocklist` | IP dentro de um CIDR de datacenter bloqueado                                                                                                                                                                                                                |

<Warning>
  Previews de mensageiros (`whatsapp`, `facebookexternalhit`, `telegrambot`) são classificados como bot: um link compartilhado no WhatsApp gera um clique `bot: true`, não um humano.
</Warning>

## Cross-device via e-mail hasheado

Quando o visitante faz signup, seu sistema pode associar o `visitorId` a um e-mail **hasheado** com `POST /track/identify`. Conversões posteriores do mesmo e-mail hasheado, em outro device, atribuem ao clique original, desde que dentro da janela de atribuição.

```mermaid theme={null}
sequenceDiagram
    participant JS as Snippet JS (seu site)
    participant API as API (rotas públicas /track/)

    JS->>API: POST /track/click { token, sub?, referrer?, visitorId? }
    API->>API: registra o clique e emite click.recorded
    API-->>JS: 201 { tracked: true, clickId, visitorId, expiresAt }
    Note over JS: visitante faz signup com e-mail
    JS->>API: POST /track/identify { visitorId, emailHash (SHA-256) }
    API->>API: usa o clique mais recente do visitante p/ definir a org e emite visitor.identified
    API-->>JS: 201 identidade (replay → 200, mesma identidade)
```

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.userepass.com/track/identify \
    -H "Content-Type: application/json" \
    -d '{
      "visitorId": "vis_01J9Z8K3M0XQ2R7B4D6F8H0A1C",
      "emailHash": "a1b2c3d4e5f60718293a4b5c6d7e8f90112233445566778899aabbccddeeff00"
    }'
  ```

  ```json 201 Created theme={null}
  {
    "id": "vid_01J9ZB2N4P7QX1R5T8V0C2E4G6",
    "organizationId": "org_01J9Z3K8YQ4N2T7B6F0X1A2C3D",
    "visitorId": "vis_01J9Z8K3M0XQ2R7B4D6F8H0A1C",
    "emailHash": "a1b2c3d4e5f60718293a4b5c6d7e8f90112233445566778899aabbccddeeff00",
    "createdAt": "2026-06-13T12:00:00.000Z"
  }
  ```
</CodeGroup>

O `emailHash` deve casar `^[a-f0-9]{64}$`: e-mail cru é rejeitado com **400**. A operação é idempotente por `(visitorId, emailHash)`: um replay devolve **200** com a mesma identidade e não re-emite evento. A organização da identidade vem do **clique mais recente** do visitante; um visitante sem nenhum clique responde **404** (uma identidade sem clique não tem valor para atribuição).

<Note>
  A identidade fica vinculada à organização do clique mais recente. Se o visitante clicou em links de mais de uma organização, a identidade é criada apenas na org do último clique.
</Note>

## Janela de atribuição e ciclo de vida do clique

O clique não tem um campo de status explícito: seu estado é derivado de `expiresAt`, `convertedAt` e `ipTruncatedAt`. A janela é `expiresAt = occurredAt + attributionWindowDays` do [Programa](/docs/conceitos/programas-e-regras) (default **30 dias**, faixa 1 a 365); o cookie `repass_vid` tem a mesma validade.

```mermaid theme={null}
stateDiagram-v2
    [*] --> Registrado: hit em /t/{token} ou /track/click<br/>emite click.recorded
    Registrado --> Convertido: a conversão marca convertedAt<br/>(clique vencedor da atribuição)
    Registrado --> Expirado: now > expiresAt
    Convertido --> [*]
    Expirado --> [*]

    note right of Registrado
        Ortogonal a qualquer estado:
        após 90 dias o IP é truncado
        (emite click.ips_truncated)
    end note
```

Não há deduplicação: recarregar a página gera um clique novo. "Únicos" existem apenas nos stats, via `visitorId` distinto.

## LGPD: minimização de IP após 90 dias

Para minimização de dados, o IP completo **nunca** é retido por mais de 90 dias. Após esse prazo, o IP do clique é truncado:

* IPv4 → prefixo `/24` (ex.: `200.10.20.0/24`)
* IPv6 → prefixo `/48`
* IP que não parseia → `null`

A geo derivada é **preservada** e o campo `ipTruncatedAt` é preenchido. O truncamento emite um evento-resumo `click.ips_truncated` por organização afetada.

<Warning>
  O IP completo nunca entra no histórico de eventos: o payload de `click.recorded` já é gerado com o IP **truncado**, pois esse payload é entregue nos [webhooks](/docs/webhooks/visao-geral) de saída. O IP completo fica disponível apenas no registro do clique, dentro da janela de 90 dias, como sinal antifraude.
</Warning>

## Consultar cliques (autenticado)

As rotas de consulta exigem sessão ou API key e são escopadas à sua organização.

<CodeGroup>
  ```bash Listar cliques theme={null}
  curl "https://api.userepass.com/clicks?program_id=prog_01J9...&bot=false&limit=25" \
    -H "Authorization: Bearer rstr_..."
  ```

  ```bash Detalhe do clique theme={null}
  curl "https://api.userepass.com/clicks/clk_01J9Z8K3M0XQ2R7B4D6F8H0A1C" \
    -H "Authorization: Bearer rstr_..."
  ```

  ```bash Stats do link theme={null}
  curl "https://api.userepass.com/links/link_01J9.../stats?from=2026-05-01&to=2026-06-01" \
    -H "Authorization: Bearer rstr_..."
  ```
</CodeGroup>

`GET /clicks` usa [paginação por cursor](/docs/convencoes/paginacao) e aceita os filtros `program_id`, `affiliate_id`, `link_id`, `bot`, `converted`, `occurred_after` e `occurred_before`. `GET /links/{linkId}/stats` retorna `{ linkId, period: { from, to }, clicks: { total, unique } }` no período informado por `from`/`to` (default: últimos 30 dias) **sempre excluindo bots**: `unique` conta `visitorId` distintos. Para os parâmetros completos, veja a aba Referência da API.

## Eventos emitidos

Cada mudança de estado emite um evento, disponível para você assinar via [webhooks](/docs/webhooks/visao-geral). Cliques **não registrados** (programa arquivado, afiliado pendente, link inativo) não emitem evento algum.

| Evento                | Quando                                                                                 |
| --------------------- | -------------------------------------------------------------------------------------- |
| `click.recorded`      | A cada clique registrado (redirect ou snippet). O `ip` no payload já vai **truncado**. |
| `visitor.identified`  | Na primeira associação `(visitorId, emailHash)`: replays não re-emitem.                |
| `click.ips_truncated` | Resumo por organização em cada lote da rotina de truncamento de IP.                    |

Consulte o [catálogo de eventos](/docs/webhooks/catalogo-de-eventos) para os payloads completos.

## Próximos passos

<CardGroup cols={2}>
  <Card title="Atribuição" icon="route" href="/docs/conceitos/atribuicao">
    Como o clique vencedor é escolhido (clickId → visitorId → emailHash → fingerprint) e os modelos por programa.
  </Card>

  <Card title="Conversões e fraude" icon="shield-check" href="/docs/conceitos/conversoes-e-fraude">
    Como registrar vendas server-to-server e os sinais antifraude que usam o tracking.
  </Card>

  <Card title="Links e cupons" icon="link" href="/docs/conceitos/links-e-cupons">
    Como os tokens de redirect e os sub-ids de campanha são criados.
  </Card>

  <Card title="Integração básica" icon="plug" href="/docs/guias/integracao-basica">
    Passo a passo para colocar o tracking e a primeira conversão no ar.
  </Card>
</CardGroup>
