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-Deliveryo eliddel 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.