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

# Ingestão de eventos

> Receba eventos de cobrança via webhook de gateway ou S2S.

A ingestão é a porta de entrada de fatos financeiros (uma assinatura criada, uma cobrança recorrente, um estorno, um cancelamento) vindos de provedores de pagamento para dentro do Repass. Cada fato é traduzido para um **evento normalizado**, único e agnóstico de gateway, e encaminhado para o processamento de Conversão e Comissão. Por isso, atribuição, deduplicação e cálculo de comissão são herdados integralmente desses fluxos: a ingestão não grava conversões nem comissões por conta própria.

Existem duas superfícies de entrada:

* **`POST /ingest/stripe/{organizationId}`**: webhook público do Stripe, autenticado pela assinatura HMAC do próprio Stripe (não pela sua API key).
* **`POST /ingest/custom`**: endpoint server-to-server (S2S), autenticado pela sua API key, para enviar o evento normalizado diretamente quando você não usa um gateway suportado.

E uma superfície de configuração: `GET` / `PUT /settings/stripe`, para gravar e ler (mascarado) o segredo de assinatura do webhook do Stripe da sua conta.

<Note>
  A ingestão é a forma recomendada de registrar cobranças recorrentes. Para entender o que acontece depois (atribuição ao Afiliado, criação da Comissão e tratamento de fraude), veja [Conversões e fraude](/docs/conceitos/conversoes-e-fraude).
</Note>

## O evento normalizado

Todo gateway é apenas um **tradutor** (payload do provedor → evento normalizado) mais uma **verificação de assinatura**. O Repass conhece um único contrato, uma união discriminada por `kind`:

| `kind`         | O que representa                                                             | Como referencia a Conversão                                                                                   |
| -------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `conversion`   | Nova relação cliente↔afiliado a atribuir (assinatura criada, compra avulsa). | Carrega os dados de matching (`clickId`, `visitorId`, `emailHash`, `fingerprint`, `couponCode`, `programId`). |
| `charge`       | Cobrança de ciclo 2+ de uma Conversão já existente.                          | Por `conversionId` ou `customerId`.                                                                           |
| `refund`       | Estorno total/parcial ou chargeback.                                         | Por `conversionId` ou `customerId`.                                                                           |
| `cancellation` | Cancelamento de assinatura.                                                  | Por `conversionId` ou `customerId`.                                                                           |

Os eventos `charge`, `refund` e `cancellation` não trazem dados de atribuição: apontam para uma Conversão existente por **`conversionId`** (ULID com prefixo `conv_`) **ou** por **`customerId`** (o id do cliente no gateway). Pelo menos um dos dois é obrigatório.

<Info>
  Dinheiro sempre em centavos (inteiro), nunca em float. Apenas **BRL** é suportado: eventos em outra moeda são ignorados na tradução.
</Info>

## Deduplicação por `sourceEventId`

A idempotência da ingestão é **de negócio e permanente**, indexada pelo `sourceEventId` de cada evento. As rotas de ingestão **não** usam o header `Idempotency-Key`: você não precisa enviá-lo. Em vez disso, todo evento carrega um `sourceEventId` estável, e o processamento de Conversão/Comissão garante que o mesmo `sourceEventId` produza um único efeito.

Isso significa que reentregas do gateway (o Stripe reenvia o mesmo webhook várias vezes, por exemplo) são absorvidas sem duplicar conversões ou comissões: o desfecho é simplesmente `replayed`, apontando para o efeito original.

<Warning>
  O `sourceEventId` deve identificar o **fato financeiro**, não a entrega do webhook. No tradutor do Stripe ele é o id do objeto financeiro, a invoice (`in_...`), o refund (`re_...`) ou `<sub.id>:deleted`, justamente para que reentregas do mesmo fato sejam dedupadas e para que um refund referencie a cobrança original.
</Warning>

## Desfechos da ingestão

A ingestão não tem máquina de estados própria; ela apenas encaminha o evento. Cada chamada produz exatamente um **desfecho terminal** (`outcome`), que descreve como o evento foi tratado:

```mermaid theme={null}
stateDiagram-v2
    [*] --> Recebido
    Recebido --> processed: estado novo gravado (conversão/comissão criada)
    Recebido --> replayed: sourceEventId já processado (reentrega idempotente)
    Recebido --> unmatched: sem atribuição / cliente sem conversão ativa
    Recebido --> skipped: regra de negócio impede o efeito (cliente ambíguo, sem comissões)
    Recebido --> ignored: tradutor descartou o evento (tipo não tratado, moeda etc.)
    processed --> [*]
    replayed --> [*]
    unmatched --> [*]
    skipped --> [*]
    ignored --> [*]
```

<AccordionGroup>
  <Accordion title="processed">
    O evento gerou efeito novo: Conversão criada e atribuída, cobrança registrada com Comissão nova, refund/clawback aplicado, ou cancelamento que anulou comissões. Um cancelamento que não tinha nada a anular continua `processed`, com `reason: "nothing_to_void"` (idempotente).
  </Accordion>

  <Accordion title="replayed">
    O mesmo `sourceEventId` já havia sido processado; nenhum efeito novo. É a reentrega idempotente.
  </Accordion>

  <Accordion title="unmatched">
    Uma Conversão não foi atribuída a nenhum Afiliado (`no_attribution`), ou um `charge`/`refund`/`cancellation` por `customerId` não achou Conversão ativa (`customer_not_attributed`). Nada é persistido.
  </Accordion>

  <Accordion title="skipped">
    Uma regra de negócio impede o efeito de forma legítima: cliente com mais de uma Conversão ativa (`ambiguous_customer`), chargeback contra Conversão sem comissões (`no_commissions`), ou um motivo herdado da cobrança (`no_rule`: nenhuma regra de comissão aplicável; `no_eligible_affiliate`: nenhum afiliado elegível; `recurrence_exhausted`: recorrência da regra já esgotada).
  </Accordion>

  <Accordion title="ignored">
    O tradutor decidiu não mapear o evento (tipo não tratado, moeda diferente de BRL, cliente ausente). O `reason` carrega o motivo. Só ocorre no webhook: o `/ingest/custom` não aceita esse `kind`.
  </Accordion>
</AccordionGroup>

## Webhook do Stripe

O webhook do Stripe é autenticado pela **assinatura HMAC do próprio Stripe**, não pela sua API key. A sua conta é identificada no path (`organizationId`), e a verificação usa o segredo (`whsec_...`) que você gravou em `/settings/stripe`.

```mermaid theme={null}
sequenceDiagram
    participant Stripe
    participant Repass as POST /ingest/stripe/{org}

    Stripe->>Repass: invoice.paid (corpo bruto + Stripe-Signature)
    alt sem config / conta inexistente
        Repass-->>Stripe: 401
    else configurado
        alt assinatura inválida/ausente
            Repass-->>Stripe: 401
        else válida
            Note over Repass: traduz para evento normalizado<br/>e cria a Conversão (atribuída / replayed / dedupada)
            Repass-->>Stripe: 200 { received: true, outcome, conversionId, commissionIds }
        end
    end
```

### Comportamento de status

O webhook responde **200 sempre que a assinatura é válida**, mesmo para eventos não tratados, sem match ou rejeitados por regra de negócio. Isso é deliberado: um 4xx faria o Stripe reentregar o evento indefinidamente. O campo `outcome` no corpo da resposta diz o que realmente aconteceu.

| HTTP                         | Quando                                                                                                                                                  |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200`                        | Assinatura válida. O corpo traz `received: true` e o `outcome` real (incluindo `ignored`, `unmatched`, `skipped`).                                      |
| `200` (`outcome: "skipped"`) | Erro de negócio durante o processamento (ex.: cobrança contra Conversão já estornada). É reconhecido como `skipped`, com `reason` descrevendo o motivo. |
| `401`                        | Conta sem o webhook do Stripe configurado ou inexistente; header `Stripe-Signature` ausente; ou assinatura inválida.                                    |

<Warning>
  Conta inexistente e conta sem configuração retornam o **mesmo 401** que uma assinatura inválida: decisão de segurança para não revelar quais contas existem.
</Warning>

Exemplo de resposta:

```json theme={null}
{
  "received": true,
  "outcome": "processed",
  "reason": null,
  "conversionId": "conv_01J9Z6F8K2M3N4P5Q6R7S8T9V0",
  "commissionIds": ["comm_01J9Z6F8K2M3N4P5Q6R7S8T9W1"]
}
```

### Eventos do Stripe suportados

O tradutor é determinístico: tudo que não casa com uma regra explícita vira `ignored`, e o webhook responde 200 para o Stripe parar de reentregar.

| Tipo Stripe                                  | Condição                               | Vira                             | Observação                                                                                   |
| -------------------------------------------- | -------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------- |
| `invoice.paid` / `invoice.payment_succeeded` | `billing_reason = subscription_create` | `conversion` (assinatura criada) | `amountCents = invoice.amount_paid`; cliente por `invoice.customer`.                         |
| `invoice.paid` / `invoice.payment_succeeded` | ciclo recorrente / update / threshold  | `charge`                         | Cobrança de ciclo 2+ resolvida por `customerId`.                                             |
| `checkout.session.completed`                 | `mode = payment`, BRL                  | `conversion` (compra avulsa)     | Cliente por `customer`, senão e-mail, senão id da sessão.                                    |
| `checkout.session.completed`                 | `mode = subscription`                  | `ignored`                        | A conversão canônica vem do `invoice.paid subscription_create`.                              |
| `charge.refunded`                            | tem refund e customer                  | `refund` (`chargeback: false`)   | Usa o refund mais recente como delta; `sourceEventId = refund.id`.                           |
| `charge.dispute.created`                     | com `secretKey` (`sk_…`)               | `refund` (`chargeback: true`)    | Lookup do charge na API Stripe; sem `secretKey` → `ignored` (`dispute_requires_api_lookup`). |
| `customer.subscription.deleted`              | tem customer                           | `cancellation`                   | `sourceEventId = <sub.id>:deleted`.                                                          |
| qualquer outro                               | n/a                                    | `ignored`                        | `unhandled_event_type`.                                                                      |

A atribuição de uma assinatura vem da **metadata** do objeto Stripe. O tradutor procura `repass_cid` (clickId) e `repass_pid` (programId) na metadata da invoice, do line item e do `subscription_details` (incluindo o layout da API Stripe 2025+). O `clickId` só é aceito se prefixado `clk_`; o `programId`, se prefixado `prog_`.

<Warning>
  Conectar o webhook **não** atribui afiliados sozinho. Seu checkout precisa propagar `repass_cid` (o `clk_...` capturado no [tracking](/docs/conceitos/tracking)) — e, opcionalmente, `repass_pid` — na metadata da assinatura (`subscription_data.metadata`) ou da session (`mode: payment`). Sem isso, o desfecho é `unmatched`. Guia completo com snippets: [Integrar Stripe](/docs/guias/integrar-stripe).
</Warning>

## Ingestão custom (S2S)

Quando você não usa um gateway suportado, envie o evento normalizado diretamente para `POST /ingest/custom`. Diferente do webhook, este endpoint é autenticado pela sua API key e **propaga erros de negócio** como 4xx normais (404/409) em vez de mascará-los como `skipped`.

O body é a mesma união discriminada por `kind`, **sem** a variante `ignored`.

<Tabs>
  <Tab title="conversion">
    ```bash theme={null}
    curl https://api.userepass.com/ingest/custom \
      -H "Authorization: Bearer rstr_..." \
      -H "Content-Type: application/json" \
      -d '{
        "kind": "conversion",
        "type": "subscription_created",
        "amountCents": 9990,
        "currency": "BRL",
        "sourceEventId": "billing_2026_06_13_abc",
        "customer": { "id": "cus_ext_42", "email": "cliente@exemplo.com" },
        "emailHash": "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3",
        "clickId": "clk_01J9Z6F8K2M3N4P5Q6R7S8T9V0",
        "programId": "prog_01J9Z6F8K2M3N4P5Q6R7S8T9V0"
      }'
    ```
  </Tab>

  <Tab title="charge">
    ```bash theme={null}
    curl https://api.userepass.com/ingest/custom \
      -H "Authorization: Bearer rstr_..." \
      -H "Content-Type: application/json" \
      -d '{
        "kind": "charge",
        "customerId": "cus_ext_42",
        "amountCents": 9990,
        "currency": "BRL",
        "sourceEventId": "invoice_2026_07_13_abc",
        "occurredAt": "2026-07-13T00:00:00Z"
      }'
    ```
  </Tab>

  <Tab title="refund">
    ```bash theme={null}
    curl https://api.userepass.com/ingest/custom \
      -H "Authorization: Bearer rstr_..." \
      -H "Content-Type: application/json" \
      -d '{
        "kind": "refund",
        "conversionId": "conv_01J9Z6F8K2M3N4P5Q6R7S8T9V0",
        "refundedAmountCents": 9990,
        "sourceEventId": "refund_2026_07_20_xyz",
        "chargeSourceEventId": "invoice_2026_07_13_abc",
        "chargeback": false
      }'
    ```
  </Tab>

  <Tab title="cancellation">
    ```bash theme={null}
    curl https://api.userepass.com/ingest/custom \
      -H "Authorization: Bearer rstr_..." \
      -H "Content-Type: application/json" \
      -d '{
        "kind": "cancellation",
        "customerId": "cus_ext_42",
        "sourceEventId": "sub_ext_99:deleted"
      }'
    ```
  </Tab>
</Tabs>

Erros do `/ingest/custom`:

| HTTP  | code                | Quando                                                                                                               |
| ----- | ------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `400` | `parameter_invalid` | `kind` desconhecido, `charge`/`refund`/`cancellation` sem `conversionId` nem `customerId`, ou valores fora de faixa. |
| `401` | `unauthorized`      | Sem API key válida.                                                                                                  |
| `404` | `not_found`         | `conversionId` explícito informado, mas inexistente.                                                                 |
| `409` | `conflict`          | Conflito de negócio (ex.: cobrança nova contra Conversão já estornada).                                              |

<Note>
  Chargebacks via `charge.dispute.created` entram pelo webhook **quando** a `secretKey` (`sk_…`) está configurada. Sem ela, o evento é `ignored` (`dispute_requires_api_lookup`) e você pode registrar o chargeback pelo `/ingest/custom` com `"chargeback": true`. Veja [Refund e clawback](/docs/guias/refund-e-clawback) e [Integrar Stripe](/docs/guias/integrar-stripe).
</Note>

### Resolução de Conversão e casos de borda

Para `charge`, `refund` e `cancellation`, a Conversão é resolvida assim:

<Steps>
  <Step title="conversionId explícito">
    Busca por id. Se não existir, retorna **404**: a referência enviada está errada.
  </Step>

  <Step title="customerId">
    Busca Conversões **ativas** do cliente: zero ⇒ `unmatched` (`customer_not_attributed`); exatamente uma ⇒ usa essa; mais de uma ⇒ `skipped` (`ambiguous_customer`). Nunca chuta.
  </Step>
</Steps>

Quando uma "nova assinatura" chega para um cliente que **já tem** Conversão ativa no mesmo Programa, a ingestão não cria Conversão nova: trata o evento como uma cobrança de ciclo subsequente da Conversão original.

## Configuração do Stripe

O segredo de assinatura do webhook (`whsec_...`) é gravado **por conta**. Ele é usado apenas para a verificação HMAC dos webhooks recebidos do Stripe.

<CodeGroup>
  ```bash PUT (grava o segredo) theme={null}
  curl -X PUT https://api.userepass.com/settings/stripe \
    -H "Authorization: Bearer rstr_..." \
    -H "Content-Type: application/json" \
    -d '{ "webhookSecret": "whsec_AbC123..." }'
  ```

  ```bash GET (lê mascarado) theme={null}
  curl https://api.userepass.com/settings/stripe \
    -H "Authorization: Bearer rstr_..."
  ```
</CodeGroup>

A resposta é sempre **mascarada**, nunca devolve o segredo completo:

```json theme={null}
{
  "configured": true,
  "webhookSecretLast4": "C123"
}
```

<Warning>
  O `webhookSecret` deve começar com `whsec_` e ter no máximo 255 caracteres, senão a gravação falha com `400`. O segredo nunca é devolvido em claro nas respostas nem aparece no histórico de eventos, onde é registrado apenas de forma mascarada (`whsec_***<last4>`).
</Warning>

## Suporte a outros gateways

O modelo do Repass é deliberadamente agnóstico de gateway: o contrato normalizado e o processamento de Conversão/Comissão são únicos, e cada provedor entra apenas como uma tradução do seu payload para o evento normalizado mais a verificação de assinatura do webhook. Na prática, isso significa que:

* A atribuição, o cálculo de comissão, o clawback e o void de comissões funcionam da mesma forma, independentemente de qual gateway originou o fato financeiro.
* Enquanto o seu gateway não tiver um webhook nativo no Repass, você pode integrá-lo hoje enviando os mesmos eventos normalizados (`conversion` / `charge` / `refund` / `cancellation`) pelo `/ingest/custom`. Use o id do **fato financeiro** do provedor como `sourceEventId` para herdar a deduplicação.

## Próximos passos

<CardGroup cols={2}>
  <Card title="Integrar Stripe" icon="credit-card" href="/docs/guias/integrar-stripe">
    Metadata `repass_cid`, Connect vs manual e checklist de atribuição.
  </Card>

  <Card title="Conversões e fraude" icon="shield-check" href="/docs/conceitos/conversoes-e-fraude">
    O que acontece depois da ingestão: atribuição, criação da Conversão e filtros antifraude.
  </Card>

  <Card title="Comissões" icon="coins" href="/docs/conceitos/comissoes">
    Como uma cobrança vira Comissão, e como refund vira void ou clawback.
  </Card>

  <Card title="Conversões server-to-server" icon="server" href="/docs/guias/conversoes-server-to-server">
    Guia prático de envio de eventos pelo `/ingest/custom`.
  </Card>

  <Card title="Refund e clawback" icon="rotate-left" href="/docs/guias/refund-e-clawback">
    Registrar estornos e chargebacks de ponta a ponta.
  </Card>
</CardGroup>
