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

# Afiliados

> Ciclo de vida do afiliado: cadastro, aprovação, tiers e termos.

Um Afiliado é uma **participação** de um parceiro em um Programa específico: um nome e e-mail vinculados a um `program` (e, opcionalmente, a um usuário da plataforma), com status, tier e dados de pagamento/fiscais próprios. O mesmo parceiro pode ter registros independentes em programas diferentes, cada um com seu próprio ciclo de vida.

Esta página descreve como afiliados entram em um programa, como são aprovados, como mudam de tier e como seu estado evolui ao longo do tempo. Para o cálculo de quanto recebem, veja [Comissões](/docs/conceitos/comissoes); para os links que geram, veja [Links e cupons](/docs/conceitos/links-e-cupons).

## Anatomia de um afiliado

O recurso `affiliate` usa o prefixo de ID `aff_` seguido de um ULID (veja [IDs e recursos](/docs/convencoes/ids-e-recursos)). Campos relevantes ao negócio:

<ResponseField name="id" type="string">
  Identificador do afiliado (`aff_` + ULID).
</ResponseField>

<ResponseField name="programId" type="string">
  Programa (`prog_`) ao qual a participação pertence.
</ResponseField>

<ResponseField name="name" type="string">
  Nome do afiliado (1 a 120 caracteres).
</ResponseField>

<ResponseField name="email" type="string">
  E-mail do afiliado. **Sempre normalizado para minúsculas** e único por programa.
</ResponseField>

<ResponseField name="status" type="string">
  Estado do ciclo de vida: `pending`, `approved`, `rejected`, `paused` ou `banned`. Resolvido na criação a partir do `mode` e do `approvalMode` do programa (ver a seção Aprovação abaixo).
</ResponseField>

<ResponseField name="tier" type="string | null">
  Nome do tier. Validado contra os tiers do programa quando o programa os define; texto livre quando não.
</ResponseField>

<ResponseField name="customCommissionRuleId" type="string | null">
  Regra de comissão específica deste afiliado (`comm_rule_`), sobrepondo a do programa/tier. Deve pertencer ao mesmo programa.
</ResponseField>

<ResponseField name="acceptedTermsVersion" type="integer | null">
  Versão dos termos do programa aceita pelo afiliado.
</ResponseField>

<ResponseField name="payoutMethod" type="string">
  Método de pagamento: `pix`, `bank_transfer`, `wise` ou `paypal`. Default `pix`.
</ResponseField>

<ResponseField name="pixKey" type="string | null">
  Chave PIX (1 a 140 caracteres). Acoplada a `pixKeyType`: ambas definidas ou ambas nulas.
</ResponseField>

<ResponseField name="pixKeyType" type="string | null">
  Tipo da chave PIX: `cpf`, `cnpj`, `email`, `phone` ou `random`.
</ResponseField>

<ResponseField name="cnpj" type="string | null">
  CNPJ normalizado a 14 dígitos (sem máscara). Todo afiliado é pessoa jurídica; o CNPJ é insumo da validação de nota fiscal quando a política `nfMode` a exige (veja [Fiscal](/docs/conceitos/fiscal)).
</ResponseField>

<Info>
  Dinheiro é sempre representado em centavos (inteiros) e percentuais em basis points. O saldo do afiliado usa moeda fixa `BRL`.
</Info>

## Ciclo de vida

O status do afiliado segue uma máquina de estados explícita. A criação resolve o estado inicial (`pending` ou `approved`); a partir daí, o status só muda pelas ações dedicadas.

```mermaid theme={null}
stateDiagram-v2
    [*] --> pending: criar (convite, aprovação manual / whitelist sem match)
    [*] --> approved: criar (direto OU aprovação automática OU whitelist com match)
    pending --> approved: approve
    pending --> rejected: reject (exige reason)
    approved --> paused: pause
    paused --> approved: resume
    approved --> banned: ban (exige reason)
    paused --> banned: ban (exige reason)
    rejected --> [*]
    banned --> [*]
```

`rejected` e `banned` são estados **terminais**: não há transição de saída. Qualquer transição não listada (por exemplo `pause` a partir de `pending`, ou `approve` a partir de `approved`) é rejeitada com `409 Conflict` (param `status`). Veja [Erros](/docs/convencoes/erros) para o envelope completo.

| Ação      | De → Para                      | Exige `reason` | Evento               | Efeito colateral                               |
| --------- | ------------------------------ | -------------- | -------------------- | ---------------------------------------------- |
| `approve` | `pending` → `approved`         | não            | `affiliate.approved` | Cria o link default (idempotente).             |
| `reject`  | `pending` → `rejected`         | sim            | `affiliate.rejected` | Nenhum.                                        |
| `pause`   | `approved` → `paused`          | não            | `affiliate.paused`   | Novas conversões não geram comissão.           |
| `resume`  | `paused` → `approved`          | não            | `affiliate.resumed`  | Não recria link default.                       |
| `ban`     | `approved`/`paused` → `banned` | sim            | `affiliate.banned`   | Revisão das comissões existentes (ver abaixo). |

### Efeitos por estado

* **`pending`**: afiliado pode acessar o portal, mas os links ainda não rastreiam (cliques redirecionam sem registrar).
* **`approved`**: operação plena; o afiliado recebe um link default automaticamente.
* **`paused`**: links continuam redirecionando e cliques são registrados, mas novas conversões não geram comissão (motivo `affiliate_paused`). Comissões existentes seguem seu ciclo normalmente.
* **`rejected`**: portal em modo somente leitura do próprio cadastro; links inativos.
* **`banned`**: links inativos imediatamente; aciona a revisão das comissões existentes e bloqueia payouts futuros.

## Modos de cadastro

Há dois modos de criação via API, controlados pelo campo `mode` em `POST /programs/:programId/affiliates` (default `direct`):

<AccordionGroup>
  <Accordion title="direct: cadastro direto pelo operador">
    O operador cria o afiliado já aprovado. **Ignora o `approvalMode` do programa**: entra `approved` mesmo em programa com aprovação `manual`, e recebe o link default na criação.
  </Accordion>

  <Accordion title="invite: convite individual por e-mail">
    O estado inicial é resolvido pelo `approvalMode` do programa (ver abaixo). Use quando o parceiro precisa passar pelo fluxo de aprovação do programa.
  </Accordion>
</AccordionGroup>

<Note>
  Para migrar histórico de outra plataforma, a criação aceita os campos `importedTotalEarnedCents` e `importedSince`: metadados informativos do histórico pré-migração que **não** geram eventos de comissão nativos.
</Note>

## Aprovação

Para `mode=invite`, o estado inicial vem do `approvalMode` configurado no Programa (veja [Programas e regras](/docs/conceitos/programas-e-regras)):

| `approvalMode`     | Resultado                                                                                                                                                          |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `automatic`        | Entra `approved` imediatamente.                                                                                                                                    |
| `manual`           | Entra `pending`; o operador aprova ou rejeita depois.                                                                                                              |
| `domain_whitelist` | Entra `approved` se o domínio do e-mail estiver na whitelist do programa (`approvalDomainWhitelist`, matching case-insensitive); caso contrário, cai em `pending`. |

<Warning>
  Whitelist vazia ou nula faz **todo** afiliado por convite cair em `pending`: `domain_whitelist` se comporta como `manual` quando não há domínios cadastrados.
</Warning>

Afiliados que entram `approved` (na criação direta, por aprovação automática ou via ação `approve`) recebem um **link default** automaticamente. A criação do link é idempotente: se já existe um link default, o existente é retornado, por isso re-aprovações nunca duplicam links. A ação `resume` **não** recria o link default.

### Exemplos

<CodeGroup>
  ```bash Criar (convite, aprovação pelo programa) theme={null}
  curl https://api.userepass.com/programs/prog_01J9.../affiliates \
    -H "Authorization: Bearer rstr_..." \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: $(uuidgen)" \
    -d '{
      "name": "Ana Souza",
      "email": "ana@example.com",
      "mode": "invite"
    }'
  ```

  ```bash Aprovar um afiliado pending theme={null}
  curl https://api.userepass.com/affiliates/aff_01J9.../approve \
    -X POST \
    -H "Authorization: Bearer rstr_..." \
    -H "Idempotency-Key: $(uuidgen)"
  ```

  ```bash Banir (exige reason) theme={null}
  curl https://api.userepass.com/affiliates/aff_01J9.../ban \
    -X POST \
    -H "Authorization: Bearer rstr_..." \
    -H "Content-Type: application/json" \
    -d '{ "reason": "fraude confirmada na investigação INV-0042" }'
  ```
</CodeGroup>

```json Resposta da criação (201) theme={null}
{
  "id": "aff_01J9X2QER8K7N0M4VYBF3T6W1A",
  "programId": "prog_01J9X1...",
  "name": "Ana Souza",
  "email": "ana@example.com",
  "status": "pending",
  "tier": null,
  "payoutMethod": "pix",
  "acceptedTermsVersion": null
}
```

<Tip>
  Todas as ações de transição (`approve`, `reject`, `pause`, `resume`, `ban`, `tier`) são POSTs idempotentes: envie um `Idempotency-Key` para tornar retries seguros. Veja [Idempotência](/docs/convencoes/idempotencia).
</Tip>

A criação e a aprovação seguem este fluxo:

```mermaid theme={null}
sequenceDiagram
    actor Operador
    participant API
    participant Programa
    participant Afiliado
    participant Link

    Operador->>API: POST /programs/:id/affiliates {name,email,mode}
    API->>Programa: valida programa (existe, não arquivado)
    API->>Afiliado: valida e-mail único no programa
    API->>Afiliado: resolve status (direct=approved / invite=approvalMode)
    Afiliado-->>API: affiliate.created
    alt status == approved
        API->>Link: cria link default (idempotente)
    end
    API-->>Operador: 201 affiliate
    Note over Operador,Link: Se pending, o operador aprova depois:
    Operador->>API: POST /affiliates/:id/approve
    API->>Link: cria link default se ainda não existe
    API-->>Operador: 200 affiliate (approved)
```

## Banimento e tratamento de comissões

`ban` (e `reject`) exige um `reason` no corpo. A ausência retorna `400` (param `reason`). A razão entra no payload do evento. O banimento **não apaga histórico**: os dados são retidos por obrigação de auditoria financeira.

Ao banir, as comissões existentes do afiliado são tratadas automaticamente (a operação é idempotente, então repeti-la não gera efeitos duplicados):

* Comissões `pending` → `voided` (razão `affiliate_banned`, evento `commission.voided`).
* Comissões `approved` ainda não pagas → entram em **revisão manual** (`underReview`, evento `commission.review_required`).
* Comissões já **pagas** permanecem intactas.

<Note>
  Os eventos `commission.*` gerados pela revisão de comissões pertencem às comissões e **não** aparecem na timeline do afiliado, que lista apenas eventos do próprio afiliado. Veja [Conversões e fraude](/docs/conceitos/conversoes-e-fraude) para o ciclo de comissões.
</Note>

## Tiers e mudança de tier

Um Programa pode definir tiers ordenados (por exemplo bronze → prata → ouro), cada um com seu próprio histórico (opcional) de regra de comissão; um tier sem regra própria usa a regra padrão do programa. A mudança de tier é feita por `POST /affiliates/:affiliateId/tier` com `{ "tier": "ouro" }` (ou `{ "tier": null }` para limpar).

```bash theme={null}
curl https://api.userepass.com/affiliates/aff_01J9.../tier \
  -X POST \
  -H "Authorization: Bearer rstr_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "tier": "ouro" }'
```

Regras de validação:

* **Tier deve existir** quando o programa define tiers; um nome desconhecido retorna `404` (param `tier`). Se o programa não define tiers, qualquer texto é aceito.
* **Limpar o tier** (`null`) é sempre permitido.
* **No-op:** trocar para o tier atual retorna o afiliado sem emitir evento.
* **Mudança vale para o futuro:** afeta apenas comissões de conversões **futuras**. Para aplicar retroativamente, use [Reprocessamento](/docs/conceitos/reprocessamento).

<Warning>
  **Sem downgrade no meio de um ciclo de payout.** Quando o programa define tiers e o tier-alvo tem posição inferior à atual (downgrade), a troca é bloqueada com `409` (param `tier`) se o afiliado tiver um payout `scheduled` ou `processing`. Isso evita comissões calculadas com um tier e pagas com expectativa de outro dentro do mesmo extrato. Upgrades passam mesmo com payout aberto; o downgrade volta a ser permitido após o ciclo fechar.
</Warning>

Cada troca efetiva emite `affiliate.tier_changed` com `before` e `after` (nomes de tier, podendo ser `null`).

## Aceite de termos

O cadastro do afiliado está sujeito ao aceite dos **termos versionados do programa**. Cada programa mantém sua própria sequência de versões, chaveada por `(programId, version)`. Veja [Termos](/docs/conceitos/termos).

Quando um afiliado aceita uma versão, o campo `acceptedTermsVersion` é gravado e o evento `affiliate.terms_accepted` é emitido (com `programId` e `termsVersion`). A versão precisa existir, caso contrário a operação retorna `404` (param `termsVersion`).

<Note>
  A API de termos publica e lista as versões por programa. Ao registrar um aceite, `acceptedTermsVersion` é gravado no afiliado e `affiliate.terms_accepted` é emitido. Veja [Termos](/docs/conceitos/termos).
</Note>

## Saldo e timeline

Cada afiliado tem dois recursos de consulta derivados:

<CardGroup cols={2}>
  <Card title="Saldo" icon="wallet">
    `GET /affiliates/:affiliateId/balance`: resumo financeiro derivado das comissões.
  </Card>

  <Card title="Timeline" icon="clock-rotate-left">
    `GET /affiliates/:affiliateId/timeline`: eventos de domínio do afiliado, em ordem cronológica de auditoria.
  </Card>
</CardGroup>

O saldo (sempre em centavos, moeda `BRL`) traz:

<ResponseField name="pendingCents" type="integer">Comissões pendentes.</ResponseField>
<ResponseField name="approvedCents" type="integer">Comissões aprovadas e ainda não pagas.</ResponseField>
<ResponseField name="underReviewCents" type="integer">Comissões em revisão manual.</ResponseField>
<ResponseField name="clawbackCents" type="integer">Estornos (débitos).</ResponseField>

<ResponseField name="nextPayoutProjectionCents" type="integer">
  Projeção do próximo payout = `max(0, approvedCents + clawbackCents)`. A projeção **nunca é negativa**: um débito que excede o aprovado não vira cobrança, rola para payouts futuros.
</ResponseField>

```json GET /affiliates/aff_01J9.../balance theme={null}
{
  "affiliateId": "aff_01J9X2QER8K7N0M4VYBF3T6W1A",
  "currency": "BRL",
  "pendingCents": 12500,
  "approvedCents": 48000,
  "underReviewCents": 0,
  "clawbackCents": -3000,
  "nextPayoutProjectionCents": 45000
}
```

A timeline retorna os eventos do afiliado (`affiliate.created`, `affiliate.approved`, `affiliate.tier_changed`, etc.), cada um com `id`, `type`, `payload`, `metadata`, `occurredAt` e `recordedAt`. Veja [Event store](/docs/conceitos/event-store) para como consultar e auditar eventos pela API.

## Listagem e filtros

`GET /affiliates` usa paginação por cursor (veja [Paginação](/docs/convencoes/paginacao)): `limit` (1 a 100, default 25), `starting_after`, `ending_before`. Filtros disponíveis: `program_id`, `status`, `tier` (igualdade exata) e `q` (busca case-insensitive por substring em nome **ou** e-mail).

```bash theme={null}
curl "https://api.userepass.com/affiliates?program_id=prog_01J9...&status=approved&limit=50" \
  -H "Authorization: Bearer rstr_..."
```

A resposta é `{ "data": [...], "hasMore": boolean }`. Este recurso não suporta `expand[]`.

## Regras de negócio

<AccordionGroup>
  <Accordion title="Estado inicial por modo de aprovação">
    Por convite, o estado inicial vem do `approvalMode` do programa: `automatic` → `approved`; `manual` → `pending`; `domain_whitelist` → `approved` se o domínio do e-mail estiver na whitelist (case-insensitive), senão `pending`. `mode=direct` ignora o `approvalMode` e entra sempre `approved`.
  </Accordion>

  <Accordion title="Participações independentes por programa">
    O mesmo parceiro pode participar de múltiplos programas (da mesma org ou de orgs diferentes). Cada participação é um registro `affiliate` independente, com status, tier e dados próprios. O e-mail é único por programa, não global.
  </Accordion>

  <Accordion title="Aceite de termos versionados">
    O cadastro está sujeito ao aceite dos termos versionados do programa, resolvidos por `(programId, version)`. O aceite grava `acceptedTermsVersion` e emite `affiliate.terms_accepted`.
  </Accordion>

  <Accordion title="Transições válidas e eventos">
    Transições permitidas: `pending` → `approved`/`rejected`; `approved` ↔ `paused`; `approved`/`paused` → `banned`. `rejected` e `banned` são terminais. Toda transição gera um evento próprio com o ator (`user`, `system` ou `api_key`).
  </Accordion>

  <Accordion title="Efeitos de estado e banimento">
    Cada estado tem efeitos definidos (ver acima). O banimento exige razão registrada, gera `affiliate.banned` com `reason` e ator, e dispara a revisão das comissões existentes. O histórico não é apagado (retenção para auditoria financeira).
  </Accordion>

  <Accordion title="Tiers">
    O programa pode definir tiers ordenados, cada um com seu próprio histórico (opcional) de regra de comissão; sem regra própria, o tier usa a regra padrão do programa. A mudança de tier afeta apenas conversões futuras (retroatividade só via Reprocessamento) e gera `affiliate.tier_changed`. Downgrade nunca é permitido no meio de um ciclo de payout aberto.
  </Accordion>
</AccordionGroup>

<Note>
  Todos os endpoints exigem autenticação via API key (`Authorization: Bearer rstr_...`). As operações são sempre resolvidas no escopo da sua conta. Veja [Autenticação](/docs/autenticacao).
</Note>

## Próximos passos

<CardGroup cols={2}>
  <Card title="Programas e regras" icon="layer-group" href="/docs/conceitos/programas-e-regras">
    Configure o `approvalMode`, a whitelist de domínios e os tiers que governam a entrada e a comissão dos afiliados.
  </Card>

  <Card title="Links e cupons" icon="link" href="/docs/conceitos/links-e-cupons">
    Entenda o link default criado na aprovação e como gerar links e cupons adicionais.
  </Card>

  <Card title="Comissões" icon="coins" href="/docs/conceitos/comissoes">
    Veja como o tier e a regra custom do afiliado determinam quanto ele recebe.
  </Card>

  <Card title="Termos" icon="file-signature" href="/docs/conceitos/termos">
    Publique versões de termos por programa e acompanhe os aceites.
  </Card>
</CardGroup>
