Skip to main content
A plataforma pode reenviar um webhook em caso de falha — por exemplo, se o seu servidor não respondeu a tempo ou retornou um status não-2xx. Por isso, seu endpoint deve ser idempotente: processar o mesmo evento duas vezes deve produzir o mesmo resultado que processar uma vez.

Estratégia

A abordagem recomendada é salvar um hash do payload recebido e, antes de processar qualquer evento, verificar se aquele hash já foi registrado.
Receber evento

Calcular hash do payload (SHA-256)

Hash já existe no banco? → Sim → Retornar 200 (ignorar)
    ↓ Não
Processar evento

Salvar hash no banco

Schema sugerido

CREATE TABLE webhook_events (
  hash        CHAR(64) PRIMARY KEY,  -- SHA-256 em hex
  event       VARCHAR(50) NOT NULL,
  received_at TIMESTAMP   NOT NULL DEFAULT NOW()
);

Implementação

Node.js

const crypto = require('crypto');
const express = require('express');

const app = express();

// Usar o corpo bruto como texto para garantir consistência do hash
app.use(express.text({ type: 'application/json' }));

app.post('/webhooks/plowf', async (req, res) => {
  const rawBody = req.body; // string bruta, antes de qualquer parse
  const signature = req.headers['x-webhook-signature'];

  // ... validar assinatura HMAC (ver guia de validação)

  const hash = crypto.createHash('sha256').update(rawBody).digest('hex');

  // Verificar se o evento já foi processado
  const { rowCount } = await db.query(
    'SELECT 1 FROM webhook_events WHERE hash = $1',
    [hash]
  );
  if (rowCount > 0) {
    return res.status(200).json({ received: true }); // duplicado, ignorar
  }

  // Registrar o hash e processar
  const payload = JSON.parse(rawBody);
  await db.query(
    'INSERT INTO webhook_events (hash, event) VALUES ($1, $2)',
    [hash, payload.event]
  );

  // ... lógica de negócio

  res.status(200).json({ received: true });
});

Python

import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhooks/plowf', methods=['POST'])
def webhook():
    raw_body = request.get_data()  # bytes brutos
    signature = request.headers.get('X-Webhook-Signature')

    # ... validar assinatura HMAC (ver guia de validação)

    hash_val = hashlib.sha256(raw_body).hexdigest()

    # Verificar se o evento já foi processado
    cursor.execute('SELECT 1 FROM webhook_events WHERE hash = %s', (hash_val,))
    if cursor.fetchone():
        return jsonify({'received': True}), 200  # duplicado, ignorar

    # Registrar o hash e processar
    payload = request.get_json()
    cursor.execute(
        'INSERT INTO webhook_events (hash, event) VALUES (%s, %s)',
        (hash_val, payload['event'])
    )

    # ... lógica de negócio

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

PHP

<?php

$rawBody = file_get_contents('php://input');  // bytes brutos
$hash = hash('sha256', $rawBody);
$payload = json_decode($rawBody, true);

// ... validar assinatura HMAC (ver guia de validação)

// Verificar se o evento já foi processado
$stmt = $pdo->prepare('SELECT 1 FROM webhook_events WHERE hash = ?');
$stmt->execute([$hash]);
if ($stmt->fetch()) {
    http_response_code(200);
    echo json_encode(['received' => true]);  // duplicado, ignorar
    exit;
}

// Registrar o hash e processar
$stmt = $pdo->prepare('INSERT INTO webhook_events (hash, event) VALUES (?, ?)');
$stmt->execute([$hash, $payload['event']]);

// ... lógica de negócio

http_response_code(200);
echo json_encode(['received' => true]);

Pontos importantes

  • Hash do corpo bruto: calcule o hash antes de qualquer parse JSON para garantir consistência entre reenvios.
  • Retorne 200 mesmo se duplicado: não retornar 2xx faz a plataforma continuar tentando reenviar.
  • Limpeza periódica: considere remover registros com mais de 30 dias para evitar crescimento indefinido da tabela.
Não use apenas data.uuid como chave de idempotência. O mesmo objeto pode gerar eventos distintos conforme o status muda (ex: PENDINGPAIDREFUNDED). O hash do payload completo garante que cada combinação de evento e estado seja única.