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: PENDING → PAID → REFUNDED). O hash do payload completo garante que cada combinação de evento e estado seja única.