Developer Docs
Español
Español
Cada webhook se firma con HMAC-SHA256 usando un webhookSecret único por organización. Tu endpoint debe recalcular la firma para confirmar que el evento es auténtico.
HMAC-SHA256( secret, normalizedBody + timestamp ) → hex → Base64| Header | Descripción |
|---|---|
zb-timestamp | Milisegundos Unix del momento de envío |
zb-signature | Firma generada por Zertiban |
Normalización del body
El body se normaliza antes de firmar: se parsea el JSON y se serializan las propiedades ordenadas alfabéticamente (sin espacios ni saltos de línea). Así, dos payloads equivalentes, {"b":2,"a":1} y {"a":1,"b":2}, producen la misma firma.
normalizedBody + zb-timestamp.HMAC-SHA256(webhookSecret, paso3) → digest binario.hex(digest) → bytes → Base64 = valor esperado de zb-signature.import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.apache.commons.codec.binary.Hex;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
private static final ObjectMapper MAPPER = JsonMapper.builder()
.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY).build();
public boolean isValidWebhook(String rawBody, String timestamp,
String signature, String secret) throws Exception {
// 1. Normalizar: parsear y serializar con propiedades ordenadas
String normalized = MAPPER.writeValueAsString(MAPPER.readValue(rawBody, Object.class));
// 2. HMAC-SHA256 del mensaje = body normalizado + timestamp
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] digest = mac.doFinal(
(normalized + timestamp).getBytes(StandardCharsets.UTF_8));
// 3. hex → Base64
String expected = Base64.getEncoder()
.encodeToString(Hex.encodeHexString(digest)
.getBytes(StandardCharsets.UTF_8));
return MessageDigest.isEqual(
expected.getBytes(StandardCharsets.UTF_8),
signature.getBytes(StandardCharsets.UTF_8));
}const crypto = require('crypto');
function isValidWebhook(rawBody, timestamp, signature, secret) {
const normalized = JSON.stringify(sortKeysDeep(JSON.parse(rawBody)));
const digest = crypto
.createHmac('sha256', secret)
.update(normalized + timestamp, 'utf8')
.digest();
const expected = Buffer.from(digest.toString('hex')).toString('base64');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
function sortKeysDeep(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) return obj.map(sortKeysDeep);
return Object.keys(obj)
.sort()
.reduce((acc, k) => ({ ...acc, [k]: sortKeysDeep(obj[k]) }), {});
}
// Express: usa express.raw() ANTES de express.json() para preservar el body crudoimport hmac, hashlib, base64, json
def is_valid_webhook(raw_body, timestamp, signature, secret):
normalized = json.dumps(
json.loads(raw_body), sort_keys=True, separators=(',', ':'))
digest = hmac.new(
secret.encode(), (normalized + timestamp).encode(), hashlib.sha256
).digest()
expected = base64.b64encode(digest.hex().encode()).decode()
return hmac.compare_digest(expected, signature)
# Flask: usa request.get_data(as_text=True) para leer el body crudohmac.compare_digest, MessageDigest.isEqual, crypto.timingSafeEqual) para evitar ataques de timing.401 o 403, no proceses el evento y registra el intento para auditoría.zb-timestamp está dentro de ±5 minutos para evitar ataques de replay.| Causa | Solución |
|---|---|
| Body modificado antes de verificar | Lee el body como texto crudo. En Express: express.raw(), no express.json() |
webhookSecret incorrecto | Confirma el secret con Zertiban |
| Orden de propiedades incorrecto | Serializa con claves ordenadas alfabéticamente |
| Falta el paso hex→Base64 | El algoritmo es HMAC → hex → Base64, no HMAC → Base64 directamente |
Se omite zb-timestamp | Concatena siempre normalizedBody + timestamp antes del HMAC |
Separa claramente las cuatro fases del consumo del webhook:
Así construyes consumidores idempotentes, auditables y resilientes frente a retries, desacoplados del procesamiento de negocio.