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

# Conversões server-to-server

> Envie conversões com confiabilidade via S2S e idempotência.

Uma Conversão é a prova auditável de "por que este Afiliado merece Comissão sobre este cliente". Quando você registra uma conversão pela API, o Repass roda de forma síncrona todo o pipeline de decisão (matching de identidade, Atribuição ao Clique/Cupom vencedor, resolução da regra de Comissão, scoring antifraude e geração da Comissão do ciclo 1) e congela cada decisão em snapshots imutáveis.

Esta página cobre as duas formas de enviar conversões server-to-server (S2S): `POST /conversions`, o endpoint S2S direto, e `POST /ingest/custom`, o endpoint genérico de Ingestão para quem não usa um gateway suportado. Para o conceito completo de atribuição e do motor antifraude, veja [Conversões e fraude](/docs/conceitos/conversoes-e-fraude); para a mecânica de idempotência, veja [Idempotência](/docs/convencoes/idempotencia).

## Qual endpoint usar

Os dois endpoints alimentam o mesmo motor de criação de conversões. A diferença está no contrato e na origem registrada.

| Caso                                                                              | Endpoint              | `source` registrado | Quando usar                                                                                      |
| --------------------------------------------------------------------------------- | --------------------- | ------------------- | ------------------------------------------------------------------------------------------------ |
| Enviar uma conversão pontual do seu backend                                       | `POST /conversions`   | `api`               | Você controla o momento e os identificadores de matching diretamente.                            |
| Enviar o ciclo financeiro normalizado (conversão, cobrança, refund, cancelamento) | `POST /ingest/custom` | `webhook`           | Você não tem um gateway suportado e quer um único contrato para todo o ciclo de vida financeiro. |

<Note>
  Ambos exigem autenticação por sessão (cookie) ou API key (`Authorization: Bearer rstr_...`). O `organizationId` e o ator vêm do contexto de auth. Você nunca envia o `organizationId` no corpo. Veja [Autenticação](/docs/autenticacao).
</Note>

## Registrar uma conversão com `POST /conversions`

Você precisa enviar o tipo, o valor (em centavos), o cliente, um `sourceEventId` e **ao menos um** identificador de matching. O motor decide a atribuição e devolve a conversão criada, ou explica por que não criou.

### Identificadores de matching

O matching é um fallthrough determinístico: o primeiro identificador que produzir candidatos vence. A ordem é fixa:

<Steps>
  <Step title="clickId">
    Clique explícito (`clk_<ULID>`). Só é candidato se pertence ao mesmo Programa, não é bot, não expirou e ocorreu antes da conversão. Um `clickId` inválido não falha: o motor desce para o próximo identificador.
  </Step>

  <Step title="visitorId">
    Identidade do visitante para matching cross-device.
  </Step>

  <Step title="emailHash">
    Hash SHA-256 do e-mail do cliente (hex). Se você não enviar `emailHash` mas enviar `customer.email`, o Repass deriva o hash do e-mail em minúsculas e usa tanto no matching quanto no `customerEmailHash` persistido.
  </Step>

  <Step title="fingerprint">
    Hash SHA-256 (hex) de fingerprint do dispositivo.
  </Step>
</Steps>

Além desses, você pode enviar `couponCode` (1 a 64 chars, normalizado para maiúsculas). Cupom inválido ou inativo rejeita a conversão com `404`. Quando há Cupom de um Afiliado e Clique atribuível de outro, o Repass aplica a política de conflito do Programa (`coupon_wins`, `click_wins` ou `split_50_50`). Veja [Atribuição](/docs/conceitos/atribuicao).

<Warning>
  Pelo menos um identificador é obrigatório: `clickId`, `visitorId`, `emailHash`, `fingerprint`, `couponCode` **ou** `customer.email`. Um corpo sem nenhum deles retorna `400 parameter_invalid`.
</Warning>

### Parâmetros do corpo

<ParamField body="type" type="string" required>
  Tipo da conversão: `subscription_created`, `one_time_purchase`, `trial_converted`, `upgrade` ou `custom`. O tipo `upgrade` tem semântica de dedupe especial (veja [Duplicatas e dedupe](#duplicatas-e-dedupe)).
</ParamField>

<ParamField body="amountCents" type="integer" required>
  Valor da cobrança que originou a conversão, em centavos (inteiro ≥ 0). Nunca use floats.
</ParamField>

<ParamField body="currency" type="string">
  Sempre `BRL` (único valor aceito). Default `BRL`.
</ParamField>

<ParamField body="customer" type="object" required>
  Identificação do cliente.

  <Expandable title="campos">
    <ParamField body="customer.id" type="string" required>
      Id do cliente no seu gateway/SaaS.
    </ParamField>

    <ParamField body="customer.email" type="string">
      E-mail em plaintext. Quando presente, serve de identificador de matching (deriva `emailHash`) e habilita os sinais antifraude de autorreferência e e-mail descartável.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="sourceEventId" type="string" required>
  Chave de idempotência de negócio (1 a 255 chars), permanente por organização. Veja [Idempotência por sourceEventId](#idempotencia-por-sourceeventid).
</ParamField>

<ParamField body="programId" type="string">
  `prog_<ULID>`. Necessário quando a organização tem mais de um Programa e você não envia `clickId`.
</ParamField>

<ParamField body="clickId" type="string">
  `clk_<ULID>` do Clique a atribuir.
</ParamField>

<ParamField body="visitorId" type="string">
  Identidade do visitante.
</ParamField>

<ParamField body="emailHash" type="string">
  SHA-256 hex do e-mail do cliente.
</ParamField>

<ParamField body="fingerprint" type="string">
  SHA-256 hex do fingerprint do dispositivo.
</ParamField>

<ParamField body="couponCode" type="string">
  Código do Cupom (1 a 64 chars).
</ParamField>

<ParamField body="productId" type="string">
  Produto/plano da cobrança. Confrontado com `applicableProductIds` da regra: se a regra restringe produtos e o `productId` não está na lista, a Comissão é pulada (`product_not_applicable`). Sem `productId`, comissiona normalmente.
</ParamField>

<ParamField body="occurredAt" type="string">
  Momento de negócio da conversão (ISO 8601). Default: agora.
</ParamField>

### Exemplo

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.userepass.com/conversions \
    -X POST \
    -H "Authorization: Bearer rstr_..." \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: 5f3b9c2a-1e4d-4f8a-9b6c-7d2e1a0f4c8b" \
    -d '{
      "type": "subscription_created",
      "amountCents": 4990,
      "currency": "BRL",
      "customer": { "id": "cus_9F2x", "email": "cliente@exemplo.com" },
      "clickId": "clk_01J9Z3Q7K8P2R4S6T8V0W2X4Y6",
      "productId": "plan_pro_monthly",
      "sourceEventId": "stripe_in_1P9q2x"
    }'
  ```

  ```json Resposta 201 (criada e atribuída) theme={null}
  {
    "attributed": true,
    "conversion": {
      "id": "conv_01J9ZB4M7N0Q2R4T6V8X0Z2B4D",
      "programId": "prog_01J9Z0A1B2C3D4E5F6G7H8J9K0",
      "affiliateId": "aff_01J9Z1K2L3M4N5P6Q7R8S9T0U1",
      "clickId": "clk_01J9Z3Q7K8P2R4S6T8V0W2X4Y6",
      "type": "subscription_created",
      "status": "approved",
      "amountCents": 4990,
      "currency": "BRL",
      "matchMethod": "click_id",
      "fraudDecision": "approve",
      "sourceEventId": "stripe_in_1P9q2x",
      "occurredAt": "2026-06-13T14:22:05.000Z"
    }
  }
  ```
</CodeGroup>

### Desfechos possíveis

A criação não é binária. O envelope sempre traz `attributed`, e flags como `replayed` ou `deduplicated` quando aplicável. O status HTTP reflete o desfecho:

| Desfecho                  | HTTP  | Corpo                                   | Significado                                                            |
| ------------------------- | ----- | --------------------------------------- | ---------------------------------------------------------------------- |
| Atribuída e criada        | `201` | `attributed: true`                      | Conversão nova com snapshots e Comissão do ciclo 1.                    |
| Sem vencedor              | `200` | `attributed: false`, `conversion: null` | Nenhum identificador casou e não havia Cupom. **Nada é persistido**.   |
| Replay idempotente        | `200` | `replayed: true`                        | Mesmo `sourceEventId` já processado. Header `Idempotent-Replay: true`. |
| Cliente já ativo (dedupe) | `200` | `deduplicated: true`                    | Cliente já tinha conversão ativa no Programa.                          |

<Tip>
  `attributed: false` com `200` não é erro: significa que o evento chegou mas não havia a quem atribuir. Trate-o como "nada a comissionar", não como falha de envio. Reenviar o mesmo `sourceEventId` não muda o resultado.
</Tip>

## Idempotência

Existem **duas camadas independentes** de idempotência em `POST /conversions`, em chaves diferentes. Entender a distinção evita conversões duplicadas e reentregas problemáticas.

```mermaid theme={null}
flowchart TD
    A[POST /conversions] --> B{Idempotency-Key<br/>já visto?}
    B -- sim --> C[Resposta HTTP armazenada<br/>Idempotent-Replay: true]
    B -- não --> D{sourceEventId<br/>já existe na org?}
    D -- sim --> E[Conversão existente<br/>replayed: true]
    D -- não --> F[Cria conversão<br/>+ Comissão + eventos]
```

### Idempotência por `sourceEventId`

É a idempotência **de negócio**, permanente (sem TTL) e escopada por organização. O mesmo `sourceEventId` nunca cria duas conversões, qualquer que seja o `Idempotency-Key`. Uma reentrega devolve a conversão original com `replayed: true`, sem reemitir os eventos `conversion.created` / `commission.created`.

Use como `sourceEventId` o id do fato financeiro na origem (o id da invoice, do pagamento ou do pedido no seu sistema), não um valor aleatório por requisição. Assim, quando seu sistema reenvia o mesmo fato, o Repass o reconhece.

### `Idempotency-Key` (transporte HTTP)

É a idempotência **de transporte**, escopada a organização + usuário e calculada por chave + hash do corpo. Ela curto-circuita reentregas idênticas do mesmo cliente (timeouts, retries de rede) e devolve a resposta armazenada com header `Idempotent-Replay: true`. `POST /conversions` aceita o header `Idempotency-Key`.

<Info>
  As duas camadas operam de forma independente: a HTTP protege contra reenvios idênticos do seu cliente; a de negócio (`sourceEventId`) garante unicidade do fato mesmo entre requisições com `Idempotency-Key` diferentes. Detalhes em [Idempotência](/docs/convencoes/idempotencia).
</Info>

## Duplicatas e dedupe

Além do replay por `sourceEventId`, o motor aplica dedupe por **cliente ativo**: um cliente que já tem conversão `pending` ou `approved` no mesmo Programa não gera nova atribuição: a chamada retorna a conversão original com `deduplicated: true`.

A exceção é `type: upgrade`: em vez de deduplicar, cria-se uma conversão filha vinculada à original via `parentConversionId`, herdando o Afiliado da conversão original (sem rodar matching de novo).

```mermaid theme={null}
flowchart TD
    A[Conversão recebida] --> B{Cliente já tem<br/>conversão ativa<br/>no Programa?}
    B -- não --> C[Roda matching + atribuição]
    B -- sim --> D{type == upgrade?}
    D -- não --> E[deduplicated: true<br/>retorna original]
    D -- sim --> F[Conversão filha<br/>parentConversionId<br/>herda afiliado]
```

## Estados da conversão

A conversão criada nasce em `approved` na maioria dos casos. Se o score antifraude cair na faixa de revisão, ela nasce `pending` e fica travada aguardando decisão manual, sem bloquear o resto do Programa.

```mermaid theme={null}
stateDiagram-v2
    [*] --> approved: fraudDecision approve/monitor
    [*] --> pending: fraudDecision = review
    pending --> approved: falso positivo liberado
    pending --> voided: fraude confirmada / void manual
    approved --> voided: void manual
```

O scoring é determinístico (regras com pesos, score de 0 a 1) e as faixas de decisão (`approveBelow` / `reviewAbove`) são configuráveis por organização. A mecânica completa de sinais, faixas e a fila de revisão está em [Conversões e fraude](/docs/conceitos/conversoes-e-fraude).

## Ingestão via `POST /ingest/custom`

Quando você não tem um gateway suportado, `POST /ingest/custom` recebe o **evento normalizado** do ciclo financeiro inteiro num único contrato. Toda conversão criada por aqui registra `source: "webhook"`, e a idempotência, a atribuição e a contabilidade de Comissões são herdadas integralmente dos módulos de Conversões e Comissões. Veja [Ingestão](/docs/conceitos/ingestao).

O corpo é uma união discriminada por `kind`:

<AccordionGroup>
  <Accordion title="conversion: nova relação cliente↔afiliado">
    Mesmos campos de `POST /conversions` (`type`, `amountCents`, `customer`, `sourceEventId`, identificadores de matching). Atribui e cria a conversão. Se não atribuir, o desfecho é `unmatched` (nada persistido).
  </Accordion>

  <Accordion title="charge: cobrança de ciclo 2+">
    Cobrança recorrente de uma conversão já existente. Referencia a conversão por `conversionId` ou `customerId`. A Comissão do ciclo é calculada pelo módulo de Comissões.
  </Accordion>

  <Accordion title="refund: estorno total/parcial ou chargeback">
    Referencia a cobrança original por `chargeSourceEventId` (ou `billingCycle`). Refund total de Comissão não paga vira void; parcial vira clawback proporcional. Para chargeback, envie `chargeback: true`.
  </Accordion>

  <Accordion title="cancellation: cancelamento de assinatura">
    Anula Comissões pendentes da assinatura. Idempotente: reentrega não re-anula o que já foi anulado.
  </Accordion>
</AccordionGroup>

<Warning>
  Para `charge`, `refund` e `cancellation` você precisa enviar **`conversionId` ou `customerId`** (pelo menos um). Um `conversionId` explícito inexistente retorna `404`; um `customerId` com mais de uma conversão ativa retorna o desfecho `skipped` (`ambiguous_customer`), nunca chuta.
</Warning>

### Exemplo: cobrança de ciclo 2 por `customerId`

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.userepass.com/ingest/custom \
    -X POST \
    -H "Authorization: Bearer rstr_..." \
    -H "Content-Type: application/json" \
    -d '{
      "kind": "charge",
      "customerId": "cus_9F2x",
      "amountCents": 4990,
      "currency": "BRL",
      "sourceEventId": "stripe_in_1P9q9z",
      "occurredAt": "2026-07-13T14:22:05.000Z"
    }'
  ```

  ```json Resposta 200 theme={null}
  {
    "outcome": "processed",
    "reason": null,
    "conversionId": "conv_01J9ZB4M7N0Q2R4T6V8X0Z2B4D",
    "commissionIds": ["comm_01JA1C5N8P2Q4R6T8V0X2Z4B6D"]
  }
  ```
</CodeGroup>

### Desfechos da ingestão

Toda chamada produz exatamente um `outcome` terminal:

| `outcome`   | Significado                                                                                                                   |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `processed` | Estado novo gravado (conversão criada, cobrança registrada, refund/clawback ou cancelamento aplicado).                        |
| `replayed`  | Mesmo `sourceEventId` já processado, sem efeito novo (idempotência permanente).                                               |
| `unmatched` | Conversão sem atribuição (`no_attribution`) ou `customerId` sem conversão ativa (`customer_not_attributed`). Nada persistido. |
| `skipped`   | Uma regra de negócio impede o efeito de forma legítima (ex.: `ambiguous_customer`, `no_commissions`).                         |

<Note>
  Diferente do webhook de gateway, `POST /ingest/custom` **não** mascara erros de domínio: um `conversionId` inexistente vira `404` e um conflito de domínio (ex.: cobrança contra conversão já `refunded`) vira `409`. Veja [Erros](/docs/convencoes/erros).
</Note>

### Idempotência na ingestão

A ingestão usa **apenas** a idempotência de negócio por `sourceEventId` (permanente). As rotas `/ingest/` não aceitam o header `Idempotency-Key`. Não o envie aqui. Reentregas do mesmo fato (mesmo `sourceEventId`) produzem um único efeito e retornam `replayed`. Use o id do objeto financeiro de origem como `sourceEventId` para que reentregas sejam deduplicadas corretamente.

## Boas práticas

<CardGroup cols={2}>
  <Card title="sourceEventId estável" icon="fingerprint">
    Derive-o do id do fato na origem (invoice, pedido), nunca de um UUID aleatório por requisição.
  </Card>

  <Card title="Trate attributed: false" icon="circle-question">
    Não é erro nem motivo para retry: é "sem a quem atribuir".
  </Card>

  <Card title="Centavos e basis points" icon="coins">
    Valores monetários em centavos (inteiro); percentuais em basis points. Nunca floats.
  </Card>

  <Card title="Reenvie com segurança" icon="rotate">
    Em timeouts, reenvie com o mesmo `sourceEventId` (e `Idempotency-Key` em `/conversions`).
  </Card>
</CardGroup>

## Próximos passos

<CardGroup cols={2}>
  <Card title="Idempotência" icon="key" href="/docs/convencoes/idempotencia">
    As duas camadas em detalhe e como compor as chaves.
  </Card>

  <Card title="Conversões e fraude" icon="shield-halved" href="/docs/conceitos/conversoes-e-fraude">
    Pipeline de decisão, snapshots e o motor antifraude.
  </Card>

  <Card title="Atribuição" icon="diagram-project" href="/docs/conceitos/atribuicao">
    Modelos, janela e o conflito Cupom × Clique.
  </Card>

  <Card title="Refund e clawback" icon="receipt" href="/docs/guias/refund-e-clawback">
    Estornos, chargebacks e devolução de Comissão.
  </Card>
</CardGroup>
