Segurança de Webhooks
Cada entrega inclui HMAC-SHA256 do body que de fato vai na rede. São dois headers — a assinatura não vem no formato Stripe t=,v1=.
Headers enviados
X-Apexpy-Signature: 3f1c0a9b…
X-Apexpy-Timestamp: 1735689600
User-Agent: Apexpy-Webhook/1.0
X-Apexpy-Timestamp— Unix timestamp (segundos) do envioX-Apexpy-Signature— hex HMAC-SHA256 de{timestamp}.{json}
O JSON usado na assinatura é o mesmo body do POST, com JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE.
Qual secret usar
Webhook global do painel: o secret gerado na criação do webhook. callback_url pontual: a Secret Key da conta (sk_live_… / sk_test_…).
Validação (PHP)
$secret = 'seu_secret';
$payload = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_APEXPY_TIMESTAMP'] ?? '';
$received = $_SERVER['HTTP_X_APEXPY_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $timestamp . '.' . $payload, $secret);
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit;
}
$event = json_decode($payload, true);
Validação (Node.js)
const crypto = require('crypto');
function validateWebhook(rawBody, signature, timestamp, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
Validação (Python)
import hmac, hashlib
def validate_webhook(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
f"{timestamp}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
Boas práticas
- Valide a assinatura antes de processar
- Recuse timestamps muito antigos (replay)
- Use HTTPS
- Responda 200 rápido e processe em background