Webhooks
Verificar a assinatura
Esquema igual ao do Stripe: v1 é o HMAC-SHA256 do texto "<t>.<corpo bruto>", com o segredo whsec_… inteiro como chave.
- Use o corpo bruto, os bytes exatos recebidos, antes de qualquer parse.
- Compare em tempo constante.
- Recuse
tcom mais de 5 minutos de diferença do seu relógio. - Assinatura inválida: responda
400e não processe.
Verificação
PHP
<?php
function cuidaroSignatureIsValid(string $rawBody, string $header, string $secret, int $tolerance = 300): bool
{
$parts = [];
foreach (explode(',', $header) as $piece) {
[$key, $value] = array_pad(explode('=', trim($piece), 2), 2, '');
$parts[$key] = $value;
}
if (! isset($parts['t'], $parts['v1']) || ! ctype_digit($parts['t'])) {
return false;
}
if (abs(time() - (int) $parts['t']) > $tolerance) {
return false;
}
$expected = hash_hmac('sha256', $parts['t'].'.'.$rawBody, $secret);
return hash_equals($expected, $parts['v1']);
}
$rawBody = file_get_contents('php://input');
if (! cuidaroSignatureIsValid($rawBody, $_SERVER['HTTP_CUIDARO_SIGNATURE'] ?? '', getenv('CUIDARO_WEBHOOK_SECRET'))) {
http_response_code(400);
exit;
}
$event = json_decode($rawBody, true);
// grave o evento (deduplicando pelo id) e processe depois
http_response_code(200);
Node.js
const crypto = require('node:crypto');
const express = require('express');
function cuidaroSignatureIsValid(rawBody, header, secret, toleranceSeconds = 300) {
const parts = {};
for (const piece of (header || '').split(',')) {
const i = piece.indexOf('=');
if (i > 0) parts[piece.slice(0, i).trim()] = piece.slice(i + 1).trim();
}
if (!/^\d+$/.test(parts.t || '') || !/^[0-9a-f]+$/.test(parts.v1 || '')) return false;
if (Math.abs(Math.floor(new Date().getTime() / 1000) - Number(parts.t)) > toleranceSeconds) return false;
const expected = crypto.createHmac('sha256', secret).update(parts.t + '.').update(rawBody).digest();
const received = Buffer.from(parts.v1, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(expected, received);
}
const app = express();
// express.raw: o corpo chega como Buffer, sem parse — obrigatório para a assinatura
app.post('/webhooks/cuidaro', express.raw({ type: 'application/json' }), (req, res) => {
if (!cuidaroSignatureIsValid(req.body, req.get('Cuidaro-Signature'), process.env.CUIDARO_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body.toString('utf8'));
// grave o evento (deduplicando pelo id) e processe depois
res.sendStatus(200);
});
Python
import hashlib, hmac, json, os, time
from flask import Flask, request
app = Flask(__name__)
def cuidaro_signature_is_valid(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = {}
for piece in (header or "").split(","):
key, sep, value = piece.strip().partition("=")
if sep:
parts[key] = value
t, v1 = parts.get("t", ""), parts.get("v1", "")
if not t.isdigit() or not v1:
return False
if abs(int(time.time()) - int(t)) > tolerance:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
@app.post("/webhooks/cuidaro")
def cuidaro_webhook():
raw_body = request.get_data() # bytes exatamente como chegaram
if not cuidaro_signature_is_valid(raw_body, request.headers.get("Cuidaro-Signature", ""), os.environ["CUIDARO_WEBHOOK_SECRET"]):
return "", 400
event = json.loads(raw_body)
# grave o evento (deduplicando pelo id) e processe depois
return "", 200
Referência gerada a partir do contrato OpenAPI 3.1 da API v1 (versão 2026-10-26), conferido contra o código por teste automatizado. Dúvidas: suporte@cuidaro.com.br.