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

# Receber e validar webhooks

> Como a Kiwify envia cada webhook, como confirmar que ele veio da Kiwify com o token, os valores de webhook_event_type e o que chega em reembolsos e chargebacks.

### Como o webhook é enviado

Cada evento é um `POST` para a URL cadastrada no webhook, com o payload em JSON no corpo (`Content-Type: application/json`). A Kiwify acrescenta à URL o parâmetro de query `signature`:

```
POST https://seu-servidor.com/kiwify?signature=<hmac-sha1-hex>
Content-Type: application/json

{"order_id":"…","order_status":"paid","webhook_event_type":"order_approved", …}
```

Se a URL já tiver query string, `signature` é acrescentado com `&`.

* O `token` do webhook **não é enviado** na requisição, nem em header, nem na query, nem no corpo. Ele é a chave usada para assinar.
* Não há outros headers de autenticação.
* Redirecionamentos não são seguidos. Responda direto na URL cadastrada.

### Validar a assinatura

`signature` é um **HMAC-SHA1**, em hexadecimal minúsculo (40 caracteres), calculado sobre o **corpo da requisição exatamente como chegou**, usando como chave o `token` cadastrado no webhook (campo `token` em [Criar webhook](/api-reference/webhooks/create.md), ou o token no painel).

Para validar:

1. Leia o corpo cru (os bytes recebidos), **antes** de fazer o parse do JSON.
2. Calcule `HMAC-SHA1(chave = token, mensagem = corpo cru)` em hexadecimal.
3. Compare com o `signature` da query usando comparação em tempo constante.
4. Se forem diferentes, recuse a requisição (por exemplo, com `401`).

> [!WARNING]
>
> Não calcule a assinatura sobre o JSON depois de fazer parse e serializar de novo. A ordem das chaves, os espaços e o escape de caracteres (`/`, acentos) podem mudar, e a assinatura deixa de bater.

> [!NOTE]
>
> Se o webhook não tiver `token`, a assinatura é calculada com uma chave vazia e não prova nada. Cadastre sempre um token longo e aleatório.

#### Vetor de teste

| | |
| - | - |
| Token | `kiwify-token-de-teste` |
| Corpo | `{"order_id":"9b1c2d3e-0000-4000-8000-000000000001","order_status":"paid","webhook_event_type":"order_approved"}` |
| `signature` | `bf18f352a4f981b91dfa3f6d8742a81080ab8c86` |

#### Exemplos

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

const app = express();
const KIWIFY_TOKEN = process.env.KIWIFY_WEBHOOK_TOKEN;

// express.raw mantém o corpo como Buffer, sem parse
app.post('/kiwify', express.raw({ type: 'application/json' }), (req, res) => {
  const received = String(req.query.signature || '');
  const expected = crypto.createHmac('sha1', KIWIFY_TOKEN).update(req.body).digest('hex');

  const valid = received.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  if (!valid) return res.status(401).end();

  const event = JSON.parse(req.body.toString('utf8'));
  // processe o evento…
  return res.status(200).end();
});
```

```python Python (Flask) theme={null}
import hashlib, hmac, os
from flask import Flask, request, abort

app = Flask(__name__)
KIWIFY_TOKEN = os.environ["KIWIFY_WEBHOOK_TOKEN"].encode()

@app.post("/kiwify")
def kiwify_webhook():
    body = request.get_data()  # bytes crus
    expected = hmac.new(KIWIFY_TOKEN, body, hashlib.sha1).hexdigest()
    if not hmac.compare_digest(expected, request.args.get("signature", "")):
        abort(401)
    event = request.get_json()
    # processe o evento…
    return "", 200
```

```php PHP theme={null}
<?php
$token = getenv('KIWIFY_WEBHOOK_TOKEN');
$body = file_get_contents('php://input'); // corpo cru
$expected = hash_hmac('sha1', $body, $token);

if (!hash_equals($expected, $_GET['signature'] ?? '')) {
    http_response_code(401);
    exit;
}

$event = json_decode($body, true);
// processe o evento…
http_response_code(200);
```

### Reenvio, duplicidade e replay

* Uma resposta `2xx` confirma a entrega. Qualquer outro status, um redirecionamento ou falta de resposta em até 40 segundos contam como falha.
* Entregas que falham são reenviadas automaticamente algumas vezes, com intervalo crescente (de 1 até 10 minutos). Você também pode reenviar manualmente em **Ver logs**.
* O mesmo evento pode chegar mais de uma vez. Trate cada evento de forma idempotente, usando a combinação `order_id` + `webhook_event_type`.
* A assinatura não inclui data/hora nem um identificador único da entrega. Ela prova que o corpo veio de quem tem o token e não foi alterado, mas **não protege contra replay**: uma requisição capturada e reenviada continua com assinatura válida. A deduplicação acima é o que protege contra replay. Para confirmar o estado atual de uma venda, consulte [`GET /sales/{id}`](/api-reference/sales/single).

### Trocar o token

Altere o `token` no painel ou com [Editar webhook](/api-reference/webhooks/edit.md). Os envios novos passam a ser assinados com o token novo na hora. Não existe período com os dois tokens válidos, e reenvios automáticos de entregas que falharam antes da troca ainda podem chegar assinados com o token antigo. Para trocar sem perder eventos:

1. Faça seu servidor aceitar a assinatura do token antigo **ou** do novo.
2. Troque o token na Kiwify.
3. Depois que os reenvios pendentes terminarem (espere pelo menos um dia), deixe de aceitar o token antigo.

### Testar Webhook

**Testar Webhook**, no painel, segue o mesmo contrato: `POST` em JSON, `signature` na query e HMAC-SHA1 do corpo com o token preenchido no formulário. Os dados do pedido são fictícios (ids aleatórios), e `webhook_event_type` e `order_status` correspondem ao evento escolhido.

### Valores de `webhook_event_type`

O gatilho que você ativa no webhook e o valor que chega em `webhook_event_type` no payload nem sempre têm o mesmo nome:

| Gatilho | `webhook_event_type` | `order_status` no payload |
| - | - | - |
| `boleto_gerado` | `billet_created` | `waiting_payment` |
| `pix_gerado` | `pix_created` | `waiting_payment` |
| `compra_aprovada` | `order_approved` | `paid` |
| `compra_recusada` | `order_rejected` | `refused` |
| `compra_reembolsada` | `order_refunded` | `refunded` |
| `chargeback` | `chargeback` | `chargedback` |
| `carrinho_abandonado` | não é enviado | não se aplica |
| `subscription_canceled` | `subscription_canceled` | status do pedido mais recente da assinatura |
| `subscription_late` | `subscription_late` | idem |
| `subscription_renewed` | `subscription_renewed` | idem |

O payload de carrinho abandonado tem outro formato (dados do carrinho, com `status` próprio) e não traz `webhook_event_type` nem `order_status`.

### Reembolso e chargeback em assinaturas

Reembolsar ou receber um chargeback de uma cobrança de assinatura **cancela a assinatura automaticamente**. Não é preciso cancelar em Assinaturas. Nenhuma cobrança futura é feita.

Chegam dois webhooks (cada um só se o gatilho estiver ativo):

| O que aconteceu | Evento de compra | Evento de assinatura |
| - | - | - |
| Reembolso | `compra_reembolsada` (`order_refunded`), `order_status: refunded` | `subscription_canceled` |
| Chargeback | `chargeback` (`chargeback`), `order_status: chargedback` | `subscription_canceled` |

* No reembolso, `subscription_canceled` traz `customer_access.has_access: false` e `access_until` na data do reembolso. Veja [Eventos de assinatura](/api-reference/webhooks/subscription-events.md).
* No chargeback, `customer_access` **não** é zerado: `access_until` continua no fim do período pago. Remova o acesso a partir do evento `chargeback`, não de `customer_access`.
* Se a assinatura já estava cancelada, `subscription_canceled` não é enviado de novo.

### Ambiente de testes

A Kiwify não tem ambiente sandbox nem modo de teste para a API ou para os webhooks. A API em `https://public-api.kiwify.com` sempre trabalha com dados reais.

* Para testar o recebimento de webhooks, use **Testar Webhook**, que envia dados fictícios.
* Consultar uma venda ([`GET /sales/{id}`](/api-reference/sales/single)) ou reembolsá-la ([Reembolsar venda](/api-reference/sales/refund.md)) só funciona com uma venda real da sua conta.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.