Webhooks

Eventos de PushHub en tu servidor, con firma verificable y reintentos.

Configura una URL https en Webhooks del panel, elige los eventos y, si quieres, limítala a una app. PushHub te enviará un POST con JSON cada vez que ocurra uno.

Eventos

Evento Cuándo
notification.completed Un envío terminó (con sus estadísticas)
notification.failed Un envío falló por completo
delivery.failed Entregas fallidas de un envío (agrupadas por lote)
device.invalidated Tokens que Google o Apple invalidaron (desinstalaciones), agrupados
credential.expiring / credential.expired / credential.invalid Una credencial está por vencer, venció o fue rechazada
quota.warning / quota.exceeded Llegaste al 80 % o al 100 % de la cuota mensual

Cabeceras

X-PushHub-Event: notification.completed
X-PushHub-Delivery: 01JD…        (único por intento de entrega)
X-PushHub-Timestamp: 1798012345
X-PushHub-Signature: sha256=5f2c…
X-PushHub-Attempt: 1

Verificar la firma

La firma es HMAC-SHA256(secreto, timestamp + "." + cuerpo). El secreto (whsec_…) se muestra al crear el webhook o al rotarlo. Rechaza los timestamps de más de 5 minutos (anti-replay) y compara en tiempo constante:

$body = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_PUSHHUB_TIMESTAMP'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, getenv('PUSHHUB_WEBHOOK_SECRET'));
if (abs(time() - (int) $ts) > 300 || !hash_equals($expected, $_SERVER['HTTP_X_PUSHHUB_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}
import crypto from 'node:crypto';
// Usa el cuerpo CRUDO (sin parsear): express.raw({ type: 'application/json' })
function verify(req, secret) {
  const ts = req.get('X-PushHub-Timestamp');
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(`${ts}.${req.body}`).digest('hex');
  const given = req.get('X-PushHub-Signature') ?? '';
  return Math.abs(Date.now() / 1000 - Number(ts)) <= 300
    && given.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
import hmac, hashlib, time

def verify(body: bytes, ts: str, signature: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
    return abs(time.time() - int(ts)) <= 300 and hmac.compare_digest(expected, signature)

Respuesta y reintentos

  • Responde 2xx en menos de 10 segundos. Procesa en segundo plano si tarda.
  • Si fallas, se reintenta a los 1 min, 5 min, 30 min, 2 h, 6 h, 12 h y 24 h.
  • Tras 50 fallos seguidos, el webhook se desactiva y te avisamos.
  • Usa X-PushHub-Delivery o el id del evento para ignorar duplicados.

En el panel ves cada entrega, con su respuesta, y puedes reenviar o probar el webhook.

Restricciones de seguridad

Solo URLs https públicas, en los puertos 443 u 8443, y sin redirecciones. Las IPs privadas o internas se rechazan.