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

# Webhook

> Receba notificações em tempo real sobre eventos da sua conta em qualquer endpoint HTTP.

Webhooks permitem que você receba os dados de cada evento diretamente no seu servidor via `POST` HTTP, sem precisar fazer polling na API.

<Card title="Integrando com IA?" icon="robot" href="/integrations/webhook/llms.txt">
  Copie o arquivo `llms.txt` e cole no seu assistente de IA (Claude, ChatGPT, Cursor...). Ele contém o contrato completo do webhook — payload, campos, enums e schema de banco de dados — para que a IA gere a integração por você.
</Card>

***

## Configurando no painel

<img src="https://mintcdn.com/velanapp/UdVYxj1NkOpI3EEN/images/webhook/01.png?fit=max&auto=format&n=UdVYxj1NkOpI3EEN&q=85&s=fb0eeab96b5c6965590dfb4b04fbdacc" alt="Card da integração Webhook no painel da Velan" style={{ borderRadius: "8px", maxWidth: "480px" }} width="719" height="356" data-path="images/webhook/01.png" />

Na tela de integrações, clique em **INTEGRAR** no card de Webhooks para abrir o modal de configuração.

<img src="https://mintcdn.com/velanapp/UdVYxj1NkOpI3EEN/images/webhook/02.png?fit=max&auto=format&n=UdVYxj1NkOpI3EEN&q=85&s=f3836c50e99e148a49d5b3f1fe4ffaa9" alt="Modal de configuração do Webhook" style={{ borderRadius: "8px", maxWidth: "420px" }} width="675" height="908" data-path="images/webhook/02.png" />

| Campo                                | Descrição                                                                                                                                                                                                                                  |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Ativar para todos os produtos**    | Quando ativo, o webhook recebe eventos de todos os produtos da empresa. Quando inativo, você seleciona os produtos manualmente.                                                                                                            |
| **Título**                           | Nome interno para identificar a integração no painel.                                                                                                                                                                                      |
| **URL**                              | Endpoint HTTP que receberá os eventos via `POST`.                                                                                                                                                                                          |
| **Autenticação**                     | Bearer Token ou Basic Auth — adicionados ao header `Authorization` de cada requisição.                                                                                                                                                     |
| **Order bumps em eventos separados** | Opcional (padrão: desativado). A partir da aprovação, envia a venda separada: parte principal + um evento por order bump (pedido próprio, mesmo código do painel). Veja [Vendas com order bump e upsell](#vendas-com-order-bump-e-upsell). |

***

## Campos de configuração

| Campo           | Tipo   | Obrigatório | Descrição                                                                                                    |
| --------------- | ------ | :---------: | ------------------------------------------------------------------------------------------------------------ |
| `url`           | string |      ✓      | URL que receberá os eventos via POST. Pode incluir um token diretamente na query string (ex: `?token=...`)   |
| `auth_token`    | string |      —      | Token enviado no header `Authorization: Bearer {token}`.                                                     |
| `auth_username` | string |      —      | Usuário para autenticação `Authorization: Basic`.                                                            |
| `auth_password` | string |      —      | Senha para autenticação Basic (use junto com `auth_username`).                                               |
| `secret`        | string |      —      | Chave para assinatura HMAC-SHA256 do body. Compatível com qualquer `auth_type` — pode ser usado em conjunto. |

<Tip>
  A forma mais simples de autenticar é incluir um token diretamente na URL — sem configuração extra:

  ```
  https://api.example.com.br/hook?token=e80ca832-6cb9-40df-b1ee-8a47ce408c16
  ```
</Tip>

***

## Validando que a requisição veio da Velan

Qualquer pessoa que descubra a URL do seu webhook pode tentar enviar dados falsos para ela. O campo **secret** resolve isso: você define uma senha secreta que só você e a Velan conhecem. A cada requisição, a Velan usa essa senha para gerar um código único a partir do conteúdo enviado e inclui esse código no header `X-Velan-Signature`. No seu servidor, você refaz o mesmo cálculo e compara — se os códigos batem, a requisição é legítima.

```
X-Velan-Signature: sha256={codigo_gerado}
```

<Note>
  Se você já usa autenticação por token na URL (`?token=...`), o `secret` é opcional. Use os dois juntos para uma camada extra de segurança.
</Note>

Como verificar no seu servidor:

<CodeGroup>
  ```bash cURL theme={null}
  BODY='{"event":"PURCHASE_APPROVED","version":"2.0.0"}'
  SECRET="meu-segredo-hmac"

  SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

  echo "X-Velan-Signature: sha256=$SIGNATURE"
  ```

  ```python Python theme={null}
  import hashlib
  import hmac

  def verify_signature(body: bytes, secret: str, header: str) -> bool:
      expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
      received = header.removeprefix("sha256=")
      return hmac.compare_digest(expected, received)
  ```

  ```javascript Node.js theme={null}
  const crypto = require("crypto");

  function verifySignature(body, secret, header) {
    const expected = crypto
      .createHmac("sha256", secret)
      .update(body)
      .digest("hex");
    const received = header.replace("sha256=", "");
    return crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(received)
    );
  }
  ```

  ```go Go theme={null}
  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
      "strings"
  )

  func verifySignature(body []byte, secret, header string) bool {
      mac := hmac.New(sha256.New, []byte(secret))
      mac.Write(body)
      expected := hex.EncodeToString(mac.Sum(nil))
      received := strings.TrimPrefix(header, "sha256=")
      return hmac.Equal([]byte(expected), []byte(received))
  }
  ```
</CodeGroup>

<Warning>
  Use sempre uma comparação segura contra timing attacks (`hmac.compare_digest`, `crypto.timingSafeEqual`, `hmac.Equal`) — nunca `==`.
</Warning>

***

## Payload de eventos de pedido

Todos os eventos relacionados a pedidos (`order.*`) entregam o mesmo envelope:

```json theme={null}
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "order_id": 123,
  "creation_date": 1736524800000,
  "event": "PURCHASE_APPROVED",
  "version": "2.0.0",
  "data": {
    "purchase": {
      "transaction": "abc123-uuid-do-pedido",
      "status": "APPROVED",
      "order_date": 1736524700000,
      "approved_date": 1736524800000,
      "refund_date": null,
      "full_price": {
        "value": 297.00,
        "currency_value": "BRL"
      },
      "price": {
        "value": 197.00,
        "currency_value": "BRL"
      },
      "payment": {
        "type": "CREDIT_CARD",
        "installments_number": 3
      },
      "fees": {
        "value": 13.45,
        "currency_value": "BRL"
      },
      "net_amount": {
        "value": 183.55,
        "currency_value": "BRL"
      },
      "gateway_transaction_id": "172691451499",
      "offer": {
        "code": "oferta-abc",
        "name": "Oferta Principal"
      },
      "order_bump": {
        "is_order_bump": false,
        "parent_purchase_transaction": null
      },
      "tracking": {
        "source": "google",
        "utm_source": "google",
        "utm_medium": "cpc",
        "utm_campaign": "black-friday",
        "utm_content": null,
        "utm_term": null,
        "src": null,
        "sck": null
      }
    },
    "product": {
      "id": 1,
      "name": "Curso de Marketing Digital"
    },
    "buyer": {
      "name": "João Silva",
      "first_name": "João",
      "last_name": "Silva",
      "email": "joao@exemplo.com",
      "checkout_phone": "+5511999990000",
      "document": "123.456.789-00",
      "document_type": "CPF"
    },
    "order_bumps": []
  }
}
```

<Note>
  O objeto `purchase.tracking` traz todos os parâmetros de URL capturados no checkout, com as chaves de UTM sempre presentes (`null` quando ausentes). Veja [Eventos → Rastreamento](/integrations/eventos#rastreamento-tracking).
</Note>

### Vendas com order bump e upsell

Uma venda pode reunir vários produtos — o principal, order bumps marcados no
checkout e um upsell aceito depois do pagamento. Os eventos ficam assim:

| Item da venda                   | Evento gerado                                                                                                                        |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Produto principal + order bumps | **Um único evento** `order.*` — `purchase.price` traz o **total da compra** (principal + bumps) e `data.order_bumps` lista cada bump |
| Upsell aceito                   | **Outro evento** `order.paid`, com `purchase.transaction` **próprio** (o upsell é um pedido separado, cobrado no cartão salvo)       |

O array `data.order_bumps` vem vazio (`[]`) quando a venda não tem bumps. Com
bumps, cada item traz o produto e a oferta adicionais:

```json theme={null}
"order_bumps": [
  {
    "product_id": 42,
    "product_name": "Planilha de Investimentos",
    "offer_code": "bump1234",
    "offer_name": "Planilha - 50% OFF",
    "price": 47.00,
    "amount": 47.00
  }
]
```

| Campo                         | Descrição                                                        |
| ----------------------------- | ---------------------------------------------------------------- |
| `product_id` / `product_name` | Produto liberado pelo bump                                       |
| `offer_code` / `offer_name`   | Oferta do bump como exibida no checkout                          |
| `price`                       | Preço base do bump                                               |
| `amount`                      | Valor cobrado do bump (com juros do parcelamento, quando houver) |

<Warning>
  Não some `purchase.price` com os valores de `data.order_bumps`: o total da
  compra já está em `purchase.price`. O array existe para você saber **quais
  produtos** compõem a venda (ex.: liberar acesso por produto).
</Warning>

#### Opcional: um evento por order bump

Se o seu sistema consome vendas **por produto**, ative **"Order bumps em
eventos separados"** na configuração do webhook. Com a opção ativa, cada order
bump da venda vira um **pedido próprio** na Velan (o mesmo que aparece no
painel do produtor), e o webhook recebe um evento por pedido:

| Campo                                             | Valor no evento do bump                                     |
| ------------------------------------------------- | ----------------------------------------------------------- |
| `purchase.transaction`                            | UUID **real** do pedido do bump — o mesmo exibido no painel |
| `purchase.price` / `full_price`                   | Valores **da linha do bump** (não o total)                  |
| `purchase.offer` / `data.product`                 | Oferta e produto **do bump**                                |
| `purchase.order_bump.is_order_bump`               | `true`                                                      |
| `purchase.order_bump.parent_purchase_transaction` | UUID do pedido principal                                    |

Nesse modo, o evento principal (`purchase.transaction` do pedido principal)
traz apenas o valor **da parte principal** da compra — cada bump carrega o
próprio valor no próprio evento. Somando principal + bumps você chega ao total
da venda, sem risco de contar valores em dobro.

<Note>
  Os eventos por bump são emitidos a partir da **aprovação** do pagamento
  (`order.paid`) e no estorno (`order.refunded`, total ou por item). Eventos
  anteriores ao pagamento (`order.created`, `order.abandoned`, `order.expired`,
  `order.failed`) continuam sendo um único evento da compra inteira, com os
  bumps listados em `data.order_bumps`.
</Note>

<Note>
  Com a opção **desativada** (padrão), nada muda: o evento de pedido representa
  a compra inteira, com o total em `purchase.price` e os bumps em
  `data.order_bumps`, e `purchase.order_bump.is_order_bump` é sempre `false`.
</Note>

### Mapeamento de eventos

O campo `event` no payload usa nomenclatura compatível com o padrão Hotmart:

| EventType (config) | Payload `event`            |
| ------------------ | -------------------------- |
| `order.paid`       | `PURCHASE_APPROVED`        |
| `order.refunded`   | `PURCHASE_REFUNDED`        |
| `order.created`    | `PURCHASE_WAITING_PAYMENT` |
| `order.expired`    | `PURCHASE_EXPIRED`         |
| `order.failed`     | `PURCHASE_CANCELLED`       |
| `order.abandoned`  | `PURCHASE_CART_ABANDONED`  |

### Variação de campos por evento

Todos os eventos `order.*` entregam o mesmo envelope. O que muda entre eles são os valores de três campos:

| Evento            | `purchase.status`              | `approved_date` | `refund_date` |
| ----------------- | ------------------------------ | :-------------: | :-----------: |
| `order.created`   | `WAITING_PAYMENT` ou `PENDING` |      `null`     |     `null`    |
| `order.paid`      | `APPROVED`                     |    timestamp    |     `null`    |
| `order.refunded`  | `REFUNDED`                     |    timestamp    |   timestamp   |
| `order.failed`    | `CANCELLED`                    |      `null`     |     `null`    |
| `order.expired`   | `EXPIRED`                      |      `null`     |     `null`    |
| `order.abandoned` | `PENDING`                      |      `null`     |     `null`    |

<Note>
  `approved_date` só é preenchido após confirmação do pagamento. `refund_date` só é preenchido quando o reembolso é processado. Nos demais eventos, ambos são `null`.
</Note>

### Mapeamento de status

O campo `purchase.status` segue a nomenclatura Hotmart:

| Status interno | `purchase.status` |
| -------------- | ----------------- |
| `pending`      | `PENDING`         |
| `awaiting_pix` | `WAITING_PAYMENT` |
| `paid`         | `APPROVED`        |
| `failed`       | `CANCELLED`       |
| `expired`      | `EXPIRED`         |
| `refunded`     | `REFUNDED`        |

### Mapeamento de método de pagamento

| Método interno | `payment.type`     |
| -------------- | ------------------ |
| `pix`          | `PIX`              |
| `credit_card`  | `CREDIT_CARD`      |
| `billet`       | `BILLET`           |
| `card_pix`     | `CREDIT_CARD`      |
| `two_cards`    | `TWO_CREDIT_CARDS` |

***

## Payload de lead criado

O evento `lead.created` usa um **envelope diferente** — não contém `purchase` nem `buyer`. O campo `event` é `LEAD_CREATED`:

```json theme={null}
{
  "id": "550e8400-e29b-41d4-a716-446655440001",
  "lead_id": 456,
  "creation_date": 1736524800000,
  "event": "LEAD_CREATED",
  "version": "2.0.0",
  "data": {
    "lead": {
      "code": "uuid-do-lead",
      "name": "Maria Souza",
      "email": "maria@exemplo.com",
      "phone": "+5521988880000",
      "status": "new",
      "ip": "177.0.0.1",
      "country": "BR",
      "city": "São Paulo",
      "region": "SP",
      "tracking": {
        "source": "google",
        "utm_source": "google",
        "utm_medium": "cpc",
        "utm_campaign": "black-friday",
        "utm_content": null,
        "utm_term": null,
        "src": null,
        "sck": null
      }
    },
    "product": {
      "id": 1,
      "name": "Curso de Marketing Digital",
      "type": "digital",
      "offer_code": "oferta-abc",
      "price": 197.00
    }
  }
}
```

| EventType (config) | Payload `event` |
| ------------------ | --------------- |
| `lead.created`     | `LEAD_CREATED`  |

***

## Evento de visualização de checkout (`checkout.viewed`)

Evento **opt-in** exclusivo de webhooks: disparado quando um visitante **abre** a página do checkout, antes de preencher qualquer dado. Usa um envelope próprio, sem `purchase`, `buyer` ou `lead` — nenhum dado pessoal. Deduplicado por visitante + oferta (janela de 6 horas). Veja o payload completo em [CHECKOUT\_VIEWED](/integrations/webhook/checkout-viewed).

| EventType (config) | Payload `event`   |
| ------------------ | ----------------- |
| `checkout.viewed`  | `CHECKOUT_VIEWED` |

***

## Boas práticas

* Responda com `HTTP 2xx` o mais rápido possível. Se precisar processar, enfileire a tarefa e responda imediatamente.
* A Velan faz retry automático em caso de falha. Após **10 falhas consecutivas**, a integração é desativada pelo [circuit breaker](/integrations/index#circuit-breaker).
* Use o campo `purchase.transaction` (UUID do pedido) como chave de idempotência para evitar processamento duplicado.

***

## Referência para LLMs

Para sistemas de IA ou automações que precisam entender o contrato completo deste webhook (schema detalhado de todos os campos, enums, nullability, persistência recomendada e ciclo de vida dos eventos), consulte o arquivo de referência estruturado:

[/integrations/webhook/llms.txt](/integrations/webhook/llms.txt)
