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

# Webhooks e Postbacks

> Entenda como receber notificações de mudanças de status das transações

O Nixfy Gateway oferece duas formas de receber notificações sobre mudanças de status das transações:

1. **Webhook Permanente**: configurado no painel, com **eventos assinados**, retry automático e log de entregas
2. **Postback por Transação**: URL específica no campo `postbackUrl` ao criar uma venda

<Info>
  Os dois convivem e são independentes. Se você configurar ambos, receberá as
  duas notificações — com formatos e cabeçalhos diferentes.
</Info>

## Diferenças entre Webhook e Postback

<CardGroup cols={2}>
  <Card title="Webhook Permanente" icon="link">
    * Configurado no painel
    * Você **escolhe quais eventos** quer receber
    * Assinado com HMAC-SHA256 (`X-Nixfy-Signature`)
    * Retry automático e reenvio manual
    * Cobre vendas, saques e carrinho abandonado
  </Card>

  <Card title="Postback por Transação" icon="webhook">
    * Configurado no campo `postbackUrl` da venda
    * Recebe notificação apenas da transação específica
    * Inclui `externalTransactionId`
    * Com assinatura HMAC no header `X-Signature`
  </Card>
</CardGroup>

## Webhook Permanente

Você cadastra uma URL no painel e **escolhe quais eventos** quer receber. Quando
o fato acontece, a Nixfy faz um `POST` nessa URL com o payload em JSON e
cabeçalhos que permitem validar a origem.

### Eventos disponíveis

O nome do evento viaja no cabeçalho `X-Nixfy-Event-Type` — é por essa string
que seu servidor decide o que fazer.

| Evento | Disparado quando |
| - | - |
| `sale.pending` | PIX ou boleto gerado, aguardando pagamento |
| `sale.paid` | Pagamento confirmado — hora de liberar o produto |
| `sale.refused` | A adquirente negou a cobrança |
| `sale.canceled` | Cancelada antes de ser paga |
| `sale.refunded` | Valor devolvido — revogar o acesso |
| `sale.chargeback` | Compra contestada (inclui mediação de PIX) |
| `withdrawal.pending` | Saque solicitado, em processamento |
| `withdrawal.completed` | Saque concluído |
| `withdrawal.rejected` | Saque recusado |
| `withdrawal.canceled` | Saque cancelado |
| `cart.abandoned` | Comprador preencheu os dados e não concluiu |
| `cart.recovered` | O carrinho abandonado virou venda paga |

<Note>
  Os eventos de carrinho **não são ativados automaticamente**, nem mesmo em
  webhooks antigos que recebiam tudo. Marque-os no painel se quiser recebê-los —
  a ideia é não mandar tráfego que seu servidor não espera.
</Note>

### Cabeçalhos de toda entrega

| Cabeçalho | Conteúdo |
| - | - |
| `X-Nixfy-Signature` | `sha256=<hmac hex>` — a assinatura |
| `X-Nixfy-Timestamp` | Epoch em segundos do envio; entra no cálculo do HMAC |
| `X-Nixfy-Event-Id` | Id estável do evento — use para deduplicar |
| `X-Nixfy-Event-Type` | O evento, ex. `sale.paid` |
| `X-Nixfy-Redelivery` | Vem com `true` apenas em reenvio manual |

### Validando a assinatura

A assinatura é calculada sobre o timestamp mais o corpo:

```
base       = "<timestamp>.<corpo bruto da requisição>"
assinatura = "sha256=" + HMAC_SHA256(segredo_do_webhook, base)
```

<Warning>
  Calcule o HMAC sobre os **bytes brutos** que chegaram, nunca sobre o JSON
  re-serializado. A ordem das chaves pode mudar na re-serialização, e aí a
  assinatura nunca confere.
</Warning>

Regras que seu receptor deve seguir:

1. Recalcular o HMAC sobre `timestamp.corpo`, usando os bytes brutos
2. Comparar em **tempo constante** (`timingSafeEqual`, `hash_equals`)
3. Rejeitar timestamp fora da janela de **5 minutos** — sem isso, uma requisição capturada valeria para sempre
4. Deduplicar pelo `X-Nixfy-Event-Id`
5. Responder `2xx` rápido e processar depois

<Info>
  O segredo é **exclusivo de cada webhook** e exibido **uma única vez**, no
  momento da criação. Ele não é recuperável por nenhuma leitura da API. Se
  perder, crie outro webhook.
</Info>

Exemplos completos de validação, em Node e PHP, estão no painel em
**Documentação → Validar assinatura**.

### Retry e idempotência

| Resposta do seu servidor | O que fazemos |
| - | - |
| `2xx` | Entregue, fim |
| `4xx` | **Não reenviamos** — recusa deliberada não muda com insistência |
| `5xx`, falha de rede ou timeout | Até **3 tentativas**, com espera crescente |
| Sem resposta em **10 segundos** | Abortamos a tentativa e reagendamos |

O `X-Nixfy-Event-Id` é **estável por fato**: `sale.paid` da venda 42 tem
sempre o mesmo id, em qualquer tentativa ou reenvio. "Venda pendente" e "venda
paga" da mesma venda são fatos diferentes, com ids diferentes.

<Warning>
  Guarde os ids já processados. Sem deduplicação, um reenvio faz seu sistema
  processar a mesma venda duas vezes — e entregar o produto em dobro.
</Warning>

### Log de entregas e reenvio

Toda tentativa fica registrada e visível em **Integrações → Entregas**: evento,
payload enviado, status HTTP devolvido, número de tentativas e erro.

O botão **Reenviar** manda o **mesmo payload** e o **mesmo `eventId`** — você
recebe o fato que aconteceu, não o estado atual da venda. A assinatura é
recalculada com timestamp novo, porque a original já estaria fora da janela, e a
entrega vem com `X-Nixfy-Redelivery: true`.

### Configuração

Configure no painel, em **Integrações → Webhooks**. A mesma gestão está
disponível na API, autenticada pela sessão do painel:

| Método e rota | Faz |
| - | - |
| `POST /api/webhook` | Cria. Body: `{ webhookUrl, events }`. **A resposta traz o `secret` — única vez** |
| `GET /api/webhook` | Lista seus webhooks (nunca devolve o segredo) |
| `PUT /api/webhook/:id` | Atualiza URL e/ou eventos |
| `DELETE /api/webhook/:id` | Remove |
| `GET /api/webhook/:id/deliveries` | Últimas entregas (`?limit=`, máx. 100) |
| `POST /api/webhook/deliveries/:id/resend` | Reenvia uma entrega |

<Note>
  URLs apontando para IP privado ou loopback são recusadas em produção.
</Note>

### Formato da Notificação

O webhook permanente envia um payload com os seguintes campos:

```json theme={null}
{
  "id": 789,
  "uuid": "01a1271c-a9a6-721f-938f-1bb03082bbb6",
  "userId": 1,
  "amount": "29900",
  "date": "2025-02-10T15:30:00.000Z",
  "status": "PAGO",
  "paymentMethod": "PIX",
  "customerDocument": "12345678901",
  "customerEmail": "maria.santos@example.com",
  "customerName": "Maria Silva Santos",
  "customerPhone": "11999887766",
  "pixKey": "00020126870014br.gov.bcb.pix2565pix.creditag.com.br/qr/v3/at/abc123...",
  "boletoCode": null,
  "shipping": null,
  "fee": 0,
  "saleItems": [
    {
      "id": 789,
      "saleId": 789,
      "saleUuid": "01a1271c-a9a6-721f-938f-1bb03082bbb6",
      "title": "Curso de Programação",
      "unitPrice": "29900",
      "quantity": 1,
      "tangible": false
    }
  ],
  "transactionId": 789,
  "type": "TRANSACTION"
}
```

### Campos do Webhook Permanente

<ParamField body="id" type="integer" required>
  ID numérico da transação (Sale ID), mantido para integrações antigas
</ParamField>

<ParamField body="uuid" type="string" required>
  Identificador público da transação (UUID). O mesmo `sale.uuid` devolvido na criação — use-o para casar a notificação com a venda
</ParamField>

<ParamField body="userId" type="integer" required>
  ID do usuário (vendedor)
</ParamField>

<ParamField body="amount" type="string" required>
  Valor da transação em centavos (formato string)
</ParamField>

<ParamField body="date" type="string" required>
  Data de criação da transação (ISO 8601)
</ParamField>

<ParamField body="status" type="string" required>
  Status atual da transação. Valores possíveis: `PENDENTE`, `EM_PROCESSAMENTO`, `PAGO`, `CANCELADO`, `RECUSADO`, `ESTORNADO`, `FALHA`, `CHARGEBACK`, `MED`
</ParamField>

<ParamField body="paymentMethod" type="string" required>
  Método de pagamento. Valores possíveis: `PIX`, `CREDIT_CARD`, `DEBIT_CARD`, `BOLETO`
</ParamField>

<ParamField body="customerDocument" type="string">
  Documento do cliente (CPF ou CNPJ)
</ParamField>

<ParamField body="customerEmail" type="string">
  Email do cliente
</ParamField>

<ParamField body="customerName" type="string">
  Nome completo do cliente
</ParamField>

<ParamField body="customerPhone" type="string">
  Telefone do cliente (apenas dígitos)
</ParamField>

<ParamField body="pixKey" type="string">
  Chave PIX gerada (apenas para pagamentos PIX)
</ParamField>

<ParamField body="boletoCode" type="string">
  Código do boleto (apenas para pagamentos via boleto)
</ParamField>

<ParamField body="shipping" type="string">
  Informações de entrega em formato JSON string (apenas para produtos físicos). `null` para produtos digitais.
</ParamField>

<ParamField body="fee" type="number" required>
  Taxa cobrada na transação em centavos. **Apenas no webhook permanente.**
</ParamField>

<ParamField body="saleItems" type="array" required>
  Lista de itens da transação
</ParamField>

<ParamField body="saleItems[].id" type="integer" required>
  ID do item
</ParamField>

<ParamField body="saleItems[].saleId" type="integer" required>
  ID numérico da venda (mesmo que `id`)
</ParamField>

<ParamField body="saleItems[].saleUuid" type="string" required>
  Identificador público da venda (mesmo que `uuid`)
</ParamField>

<ParamField body="saleItems[].title" type="string" required>
  Título do produto
</ParamField>

<ParamField body="saleItems[].unitPrice" type="string" required>
  Preço unitário em centavos (formato string)
</ParamField>

<ParamField body="saleItems[].quantity" type="integer" required>
  Quantidade do item
</ParamField>

<ParamField body="saleItems[].tangible" type="boolean" required>
  Se o item é físico (`true`) ou digital (`false`)
</ParamField>

<ParamField body="transactionId" type="integer" required>
  ID da transação (mesmo que `id`). **Apenas no webhook permanente.**
</ParamField>

<ParamField body="type" type="string" required>
  Tipo da notificação. Sempre `"TRANSACTION"` para webhooks de transações. **Apenas no webhook permanente.**
</ParamField>

***

## Postback por Transação

Postbacks são URLs específicas configuradas no campo `postbackUrl` ao criar uma venda. Recebem notificações apenas daquela transação específica.

### Configuração

Configure o postback incluindo o campo `postbackUrl` ao criar a transação:

```json theme={null}
{
  "amount": 29900,
  "paymentMethod": "PIX",
  "customer": {
    "name": "Maria Silva Santos",
    "email": "maria.santos@example.com"
  },
  "items": [...],
  "postbackUrl": "https://seu-servidor.com/webhook/transacao"
}
```

### Segurança

Postbacks incluem assinatura HMAC-SHA256 no header `X-Signature` para validação de segurança. A assinatura é gerada usando a chave secreta `POSTBACK_SECRET_KEY` configurada no ambiente do Nixfy Gateway.

<Warning>
  **Importante**: Sempre valide a assinatura HMAC antes de processar a notificação. Isso garante que a requisição realmente veio do Nixfy Gateway e não foi alterada durante a transmissão.
</Warning>

**Como funciona a assinatura:**

1. O Nixfy Gateway gera uma assinatura HMAC-SHA256 do payload JSON usando a chave secreta `POSTBACK_SECRET_KEY`
2. A assinatura é enviada no header HTTP `X-Signature`
3. Você deve validar a assinatura comparando com a assinatura esperada gerada localmente

**Exemplo de requisição com assinatura:**

```http theme={null}
POST https://seupostbackurl.com.br/webhook/transacao
Content-Type: application/json
X-Signature: a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456
```

**Validação da Assinatura em Node.js:**

```javascript theme={null}
const crypto = require('crypto');

function validateSignature(payload, signature, secretKey) {
  // Gerar assinatura esperada usando a mesma chave secreta
  const hmac = crypto.createHmac('sha256', secretKey);
  hmac.update(JSON.stringify(payload));
  const expectedSignature = hmac.digest('hex');
  
  // Comparar assinaturas de forma segura (timing-safe)
  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expectedSignature, 'hex')
  );
}

// Uso no endpoint
app.post('/webhook/transacao', (req, res) => {
  const signature = req.headers['x-signature'];
  const payload = req.body;
  const secretKey = process.env.POSTBACK_SECRET_KEY;
  
  if (!validateSignature(payload, signature, secretKey)) {
    return res.status(401).json({ error: 'Assinatura inválida' });
  }
  
  // Processar notificação...
});
```

**Validação da Assinatura em Python:**

```python theme={null}
import hmac
import hashlib
import json
import os

def validate_signature(payload, signature, secret_key):
    """
    Valida a assinatura HMAC-SHA256 do postback.
    
    IMPORTANTE: O JSON deve ser stringificado SEM ordenação de chaves,
    exatamente como enviado pelo Nixfy Gateway.
    """
    # Gerar assinatura esperada
    payload_json = json.dumps(payload, separators=(',', ':'))
    expected_signature = hmac.new(
        secret_key.encode('utf-8'),
        payload_json.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    # Comparar assinaturas de forma segura
    return hmac.compare_digest(signature, expected_signature)

# Uso no endpoint Flask
@app.route('/webhook/transacao', methods=['POST'])
def webhook_transacao():
    signature = request.headers.get('X-Signature')
    payload = request.json
    secret_key = os.environ.get('POSTBACK_SECRET_KEY')
    
    if not validate_signature(payload, signature, secret_key):
        return jsonify({'error': 'Assinatura inválida'}), 401
    
    # Processar notificação...
    return jsonify({'success': True}), 200
```

<Info>
  **Sobre a chave secreta**: A chave `POSTBACK_SECRET_KEY` é configurada no ambiente do Nixfy Gateway. Esta é a mesma chave que você deve usar para validar as assinaturas dos postbacks. Entre em contato com o suporte técnico para obter ou configurar sua chave secreta de postback.

  **Nota importante**: O JSON do payload é stringificado **sem ordenação de chaves** (`JSON.stringify` padrão). Não use `sort_keys=True` no Python ou qualquer ordenação de chaves, pois isso resultará em assinaturas diferentes e a validação falhará.
</Info>

### Formato da Notificação

O postback por transação envia um payload com os seguintes campos:

```json theme={null}
{
  "id": 789,
  "uuid": "01a1271c-a9a6-721f-938f-1bb03082bbb6",
  "userId": 1,
  "amount": "29900",
  "date": "2025-02-10T15:30:00.000Z",
  "status": "PAGO",
  "paymentMethod": "PIX",
  "customerDocument": "12345678901",
  "customerEmail": "maria.santos@example.com",
  "customerName": "Maria Silva Santos",
  "customerPhone": "11999887766",
  "pixKey": "00020126870014br.gov.bcb.pix2565pix.creditag.com.br/qr/v3/at/abc123...",
  "boletoCode": null,
  "shipping": null,
  "saleItems": [
    {
      "id": 789,
      "saleId": 789,
      "saleUuid": "01a1271c-a9a6-721f-938f-1bb03082bbb6",
      "title": "Curso de Programação",
      "unitPrice": "29900",
      "quantity": 1,
      "tangible": false
    }
  ],
  "externalTransactionId": "103693454"
}
```

### Campos do Postback por Transação

<ParamField body="id" type="integer" required>
  ID numérico da transação (Sale ID), mantido para integrações antigas
</ParamField>

<ParamField body="uuid" type="string" required>
  Identificador público da transação (UUID), o mesmo `sale.uuid` devolvido na criação
</ParamField>

<ParamField body="userId" type="integer" required>
  ID do usuário (vendedor)
</ParamField>

<ParamField body="amount" type="string" required>
  Valor da transação em centavos (formato string)
</ParamField>

<ParamField body="date" type="string" required>
  Data de criação da transação (ISO 8601)
</ParamField>

<ParamField body="status" type="string" required>
  Status atual da transação. Valores possíveis: `PENDENTE`, `EM_PROCESSAMENTO`, `PAGO`, `CANCELADO`, `RECUSADO`, `ESTORNADO`, `FALHA`, `CHARGEBACK`, `MED`
</ParamField>

<ParamField body="paymentMethod" type="string" required>
  Método de pagamento. Valores possíveis: `PIX`, `CREDIT_CARD`, `DEBIT_CARD`, `BOLETO`
</ParamField>

<ParamField body="customerDocument" type="string">
  Documento do cliente (CPF ou CNPJ)
</ParamField>

<ParamField body="customerEmail" type="string">
  Email do cliente
</ParamField>

<ParamField body="customerName" type="string">
  Nome completo do cliente
</ParamField>

<ParamField body="customerPhone" type="string">
  Telefone do cliente (apenas dígitos)
</ParamField>

<ParamField body="pixKey" type="string">
  Chave PIX gerada (apenas para pagamentos PIX)
</ParamField>

<ParamField body="boletoCode" type="string">
  Código do boleto (apenas para pagamentos via boleto)
</ParamField>

<ParamField body="shipping" type="string">
  Informações de entrega em formato JSON string (apenas para produtos físicos). `null` para produtos digitais.
</ParamField>

<ParamField body="saleItems" type="array" required>
  Lista de itens da transação. Cada item traz `saleId` e `saleUuid` da venda
</ParamField>

<ParamField body="externalTransactionId" type="string" required>
  ID da transação no gateway de pagamento externo (adquirente). **Apenas no postback por transação.**
</ParamField>

## Comparação dos Formatos

| Campo | Webhook Permanente | Postback por Transação |
| - | - | - |
| `id` | ✅ | ✅ |
| `uuid` | ✅ | ✅ |
| `userId` | ✅ | ✅ |
| `amount` | ✅ | ✅ |
| `date` | ✅ | ✅ |
| `status` | ✅ | ✅ |
| `paymentMethod` | ✅ | ✅ |
| `customerDocument` | ✅ | ✅ |
| `customerEmail` | ✅ | ✅ |
| `customerName` | ✅ | ✅ |
| `customerPhone` | ✅ | ✅ |
| `pixKey` | ✅ | ✅ |
| `boletoCode` | ✅ | ✅ |
| `shipping` | ✅ | ✅ |
| `saleItems` | ✅ | ✅ |
| `fee` | ✅ | ❌ |
| `transactionId` | ✅ | ❌ |
| `type` | ✅ (`"TRANSACTION"`) | ❌ |
| `externalTransactionId` | ❌ | ✅ |
| **Assinatura HMAC** | ✅ (`X-Nixfy-Signature`) | ✅ (`X-Signature`) |

## Quando as Notificações São Enviadas

As notificações são enviadas quando a transação muda para um **status final**:

* `PAGO` - Pagamento confirmado
* `CANCELADO` - Transação cancelada
* `RECUSADO` - Pagamento recusado
* `ESTORNADO` - Valor estornado
* `FALHA` - Falha no processamento
* `CHARGEBACK` - Chargeback identificado (cartão)
* `MED` - Mediação PIX iniciada

**Nota:** Status intermediários como `PENDENTE` e `EM_PROCESSAMENTO` não geram notificações.

## PIX Automático (assinaturas)

Cobranças recorrentes usam **o mesmo webhook** de qualquer outra transação — não
há endpoint nem formato separado. O que muda é o `paymentMethod`, que vem como
`PIX_AUTOMATICO`.

<Info>
  **Cada ciclo é uma transação nova**, com seu próprio `id`. Se um cliente
  assina em janeiro e você cobra mensalmente, em março você já recebeu três
  notificações independentes — não uma atualização da mesma transação.
</Info>

### A autorização não gera notificação

Vale entender a diferença entre autorizar e cobrar:

| Momento | Gera venda? | Gera webhook? |
| - | - | - |
| Cliente autoriza a recorrência (jornadas 1 e 2) | ❌ | ❌ |
| Cliente autoriza **e paga** o primeiro ciclo (jornadas 3 e 4) | ✅ | ✅ |
| Banco debita cada ciclo seguinte | ✅ | ✅ |

Nas jornadas 1 e 2 a resposta da criação já avisa: vem com `sale: null`. Você só
recebe a primeira notificação quando o débito acontecer, a partir da
`dataInicial` que você definiu.

<Warning>
  Não trate a ausência de webhook logo após a autorização como falha. Nas
  jornadas 1 e 2 isso é o comportamento correto — o cliente autorizou o débito,
  mas nenhum dinheiro se moveu ainda.
</Warning>

### Correlacionando os ciclos

Para ligar uma notificação à assinatura que a originou, use o `idRec` — o
identificador da recorrência devolvido na criação. Ele acompanha as vendas de
todos os ciclos daquele mandato, junto do `contrato` que você definiu.

<Tip>
  Guarde o `idRec` no seu banco assim que criar a autorização. É com ele que
  você cobra ciclos avulsos, consulta o estado da recorrência e cancela o
  mandato — e é o único identificador que a adquirente reconhece.
</Tip>

## Exemplo de Implementação

### Node.js/Express

```javascript theme={null}
const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

// Postback por Transação (com validação HMAC)
app.post('/webhook/transacao', (req, res) => {
  const signature = req.headers['x-signature'];
  const payload = req.body;
  const secretKey = process.env.POSTBACK_SECRET_KEY;
  
  // Validar assinatura
  const hmac = crypto.createHmac('sha256', secretKey);
  hmac.update(JSON.stringify(payload));
  const expectedSignature = hmac.digest('hex');
  
  // Comparação segura contra timing attacks
  if (!crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expectedSignature, 'hex')
  )) {
    return res.status(401).json({ error: 'Assinatura inválida' });
  }
  
  // Processar notificação
  console.log('Transação atualizada:', {
    id: payload.id,
    status: payload.status,
    amount: payload.amount
  });
  
  res.status(200).json({ success: true });
});

// Webhook Permanente — assinado, com deduplicação por evento
// Use o corpo BRUTO: express.raw({ type: 'application/json' })
const processados = new Set(); // em produção, use Redis ou banco

app.post('/webhook/permanente',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const corpoBruto = req.body.toString('utf8');
    const timestamp = req.get('X-Nixfy-Timestamp');
    const recebida = req.get('X-Nixfy-Signature') || '';
    const eventId = req.get('X-Nixfy-Event-Id');
    const evento = req.get('X-Nixfy-Event-Type');

    // Recusa entregas antigas: sem isso, uma requisição capturada valeria sempre
    if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
      return res.status(401).json({ error: 'Timestamp fora da janela' });
    }

    const esperada = 'sha256=' + crypto
      .createHmac('sha256', process.env.NIXFY_WEBHOOK_SECRET)
      .update(timestamp + '.' + corpoBruto)
      .digest('hex');

    const a = Buffer.from(esperada);
    const b = Buffer.from(recebida);
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.status(401).json({ error: 'Assinatura inválida' });
    }

    // Responda 2xx antes de processar — acima de 10s a entrega é abortada
    res.status(200).json({ success: true });

    // Deduplicação: sem isso, um reenvio entrega o produto duas vezes
    if (processados.has(eventId)) return;
    processados.add(eventId);

    const payload = JSON.parse(corpoBruto);
    if (evento === 'sale.paid') {
      console.log('Liberar produto da venda', payload.id);
    }
  });

app.listen(3000);
```

### Python/Flask

```python theme={null}
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
import os
import time

app = Flask(__name__)

def validate_signature(payload, signature, secret_key):
    """
    Valida a assinatura HMAC-SHA256.
    IMPORTANTE: Não use sort_keys=True - o JSON deve ser stringificado
    exatamente como enviado pelo Nixfy Gateway.
    """
    payload_json = json.dumps(payload, separators=(',', ':'))
    expected_signature = hmac.new(
        secret_key.encode('utf-8'),
        payload_json.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(signature, expected_signature)

# Postback por Transação (com validação HMAC)
@app.route('/webhook/transacao', methods=['POST'])
def webhook_transacao():
    signature = request.headers.get('X-Signature')
    payload = request.json
    secret_key = os.environ.get('POSTBACK_SECRET_KEY')
    
    # Validar assinatura
    if not validate_signature(payload, signature, secret_key):
        return jsonify({'error': 'Assinatura inválida'}), 401
    
    # Processar notificação
    print(f"Transação atualizada: {payload['id']} - {payload['status']}")
    
    return jsonify({'success': True}), 200

# Webhook Permanente — assinado, com deduplicação por evento
processados = set()  # em produção, use Redis ou banco

@app.route('/webhook/permanente', methods=['POST'])
def webhook_permanente():
    corpo_bruto = request.get_data(as_text=True)  # bytes brutos, não request.json
    timestamp = request.headers.get('X-Nixfy-Timestamp', '')
    recebida = request.headers.get('X-Nixfy-Signature', '')
    event_id = request.headers.get('X-Nixfy-Event-Id')
    evento = request.headers.get('X-Nixfy-Event-Type')

    # Recusa entregas antigas: sem isso, uma requisição capturada valeria sempre
    if not timestamp or abs(time.time() - float(timestamp)) > 300:
        return jsonify({'error': 'Timestamp fora da janela'}), 401

    esperada = 'sha256=' + hmac.new(
        os.environ['NIXFY_WEBHOOK_SECRET'].encode(),
        f"{timestamp}.{corpo_bruto}".encode(),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(esperada, recebida):
        return jsonify({'error': 'Assinatura inválida'}), 401

    # Deduplicação: sem isso, um reenvio entrega o produto duas vezes
    if event_id in processados:
        return jsonify({'success': True}), 200
    processados.add(event_id)

    payload = json.loads(corpo_bruto)
    if evento == 'sale.paid':
        print(f"Liberar produto da venda {payload['id']}")

    return jsonify({'success': True}), 200

if __name__ == '__main__':
    app.run(port=3000)
```

## Boas Práticas

<Warning>
  **Importante:**

  * Sempre retorne HTTP 200 para indicar que a notificação foi recebida com sucesso
  * Implemente idempotência para evitar processamento duplicado
  * **Sempre valide a assinatura HMAC em postbacks por transação** antes de processar
  * Use comparação timing-safe para validar assinaturas (evita timing attacks)
  * Use HTTPS para proteger os dados em trânsito
  * Implemente retry logic no seu servidor caso a notificação falhe
  * Mantenha a chave `POSTBACK_SECRET_KEY` segura e nunca a exponha em código frontend
</Warning>

<Info>
  **Dica:** Para testar webhooks localmente, use ferramentas como [ngrok](https://ngrok.com) ou [localtunnel](https://localtunnel.github.io/www/) para expor seu servidor local.
</Info>


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