Un webhook es una URL tuya a la que Blixye manda un POST cada vez que pasa algo a lo que te has suscrito: una llamada nueva, una citación, un ban, un ascenso… La lista de eventos y su ámbito está en API v1: eventos. Se dan de alta, se cambian y se borran con /v1/webhooks y el ámbito webhooks:manage (API v1: webhooks); una llave solo puede suscribirse a los eventos de sus propios ámbitos.
Un webhook es una URL tuya a la que Blixye manda un POST cada vez que pasa algo a lo que te has suscrito: una llamada nueva, una citación, un ban, un ascenso… La lista de eventos y su ámbito está en API v1: eventos. Se dan de alta, se cambian y se borran con /v1/webhooks y el ámbito webhooks:manage (API v1: webhooks); una llave solo puede suscribirse a los eventos de sus propios ámbitos.
OJO
Un webhook es una URL tuya a la que Blixye manda un POST cada vez que pasa algo a lo que te has suscrito: una llamada nueva, una citación, un ban, un ascenso… La lista de eventos y su ámbito está en API v1: eventos. Se dan de alta, se cambian y se borran con /v1/webhooks y el ámbito webhooks:manage (API v1: webhooks); una llave solo puede suscribirse a los eventos de sus propios ámbitos.
El destino
El destino
OJO
El destino
OJO
Solo https, sin usuario ni contraseña en la URL, y de 500 caracteres como mucho.
Todas las IP a las que resuelve el nombre tienen que ser públicas: ni privadas, ni de loopback, ni de enlace local, ni reservadas. Se comprueba al guardar y otra vez en cada entrega.
No se siguen redirecciones: un 3xx cuenta como fallo.
Cuántos destinos puedes tener depende del plan: 1 con Plus, 5 con Premium y 20 con Business.
Lo que llega
Lo que llega
OJO
Lo que llega
OJO
Un POST con el sobre del evento en JSON: {"id", "type", "created_at", "data"}. Es el mismo sobre, con el mismo id, que da GET /v1/events.
X-Blixye-Signature: t=<hora unix>,v1=<HMAC-SHA256 en hexadecimal>.
X-Blixye-Event: el tipo de evento.
X-Blixye-Delivery: el id de la entrega, el mismo en todos sus reintentos. Úsalo, o el id del evento, para no procesar nada dos veces.
User-Agent: siempre Blixye-Webhooks/1.0.
Verificar la firma
Verificar la firma
OJO
Verificar la firma
El secreto (whsec_…) se enseña una sola vez, al dar de alta el webhook o al rotar su secreto. La firma es el HMAC-SHA256, con ese secreto, de la hora t, un punto y el cuerpo TAL CUAL llega, byte a byte: no lo vuelvas a serializar. Compara en tiempo constante y rechaza la entrega si t está a más de 5 minutos de tu hora.
El secreto (whsec_…) se enseña una sola vez, al dar de alta el webhook o al rotar su secreto. La firma es el HMAC-SHA256, con ese secreto, de la hora t, un punto y el cuerpo TAL CUAL llega, byte a byte: no lo vuelvas a serializar. Compara en tiempo constante y rechaza la entrega si t está a más de 5 minutos de tu hora.
OJO
El secreto (whsec_…) se enseña una sola vez, al dar de alta el webhook o al rotar su secreto. La firma es el HMAC-SHA256, con ese secreto, de la hora t, un punto y el cuerpo TAL CUAL llega, byte a byte: no lo vuelvas a serializar. Compara en tiempo constante y rechaza la entrega si t está a más de 5 minutos de tu hora.
En Node.js, sin dependencias:
En Node.js, sin dependencias:
OJO
En Node.js, sin dependencias:
const http = require('node:http');
const crypto = require('node:crypto');
const SECRET = process.env.BLIXYE_WEBHOOK_SECRET;
function verify(header, rawBody) {
const m = /^t=([0-9]+),v1=([a-f0-9]{64})$/.exec(header || '');
if (!m) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - Number(m[1])) > 300) return false;
const expected = crypto.createHmac('sha256', SECRET)
.update(m[1] + '.')
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]));
}
http.createServer((req, res) => {
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
const rawBody = Buffer.concat(chunks);
if (!verify(req.headers['x-blixye-signature'], rawBody)) {
res.writeHead(400).end();
return;
}
const event = JSON.parse(rawBody.toString('utf8'));
res.writeHead(204).end();
console.log(event.type, event.id, req.headers['x-blixye-delivery']);
});
}).listen(3000);
const http = require('node:http');
const crypto = require('node:crypto');
const SECRET = process.env.BLIXYE_WEBHOOK_SECRET;
function verify(header, rawBody) {
const m = /^t=([0-9]+),v1=([a-f0-9]{64})$/.exec(header || '');
if (!m) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - Number(m[1])) > 300) return false;
const expected = crypto.createHmac('sha256', SECRET)
.update(m[1] + '.')
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]));
}
http.createServer((req, res) => {
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
const rawBody = Buffer.concat(chunks);
if (!verify(req.headers['x-blixye-signature'], rawBody)) {
res.writeHead(400).end();
return;
}
const event = JSON.parse(rawBody.toString('utf8'));
res.writeHead(204).end();
console.log(event.type, event.id, req.headers['x-blixye-delivery']);
});
}).listen(3000);
OJO
const http = require('node:http');
const crypto = require('node:crypto');
const SECRET = process.env.BLIXYE_WEBHOOK_SECRET;
function verify(header, rawBody) {
const m = /^t=([0-9]+),v1=([a-f0-9]{64})$/.exec(header || '');
if (!m) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - Number(m[1])) > 300) return false;
const expected = crypto.createHmac('sha256', SECRET)
.update(m[1] + '.')
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]));
}
http.createServer((req, res) => {
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
const rawBody = Buffer.concat(chunks);
if (!verify(req.headers['x-blixye-signature'], rawBody)) {
res.writeHead(400).end();
return;
}
const event = JSON.parse(rawBody.toString('utf8'));
res.writeHead(204).end();
console.log(event.type, event.id, req.headers['x-blixye-delivery']);
});
}).listen(3000);
En PHP:
En PHP:
OJO
En PHP:
<?php
$secret = getenv('BLIXYE_WEBHOOK_SECRET');
$rawBody = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_BLIXYE_SIGNATURE'] ?? '';
if (!preg_match('/^t=([0-9]+),v1=([a-f0-9]{64})$/', $header, $m)
|| abs(time() - (int) $m[1]) > 300
|| !hash_equals(hash_hmac('sha256', $m[1] . '.' . $rawBody, $secret), $m[2])) {
http_response_code(400);
exit;
}
$event = json_decode($rawBody, true);
http_response_code(204);
error_log($event['type'] . ' ' . $event['id']);
<?php
$secret = getenv('BLIXYE_WEBHOOK_SECRET');
$rawBody = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_BLIXYE_SIGNATURE'] ?? '';
if (!preg_match('/^t=([0-9]+),v1=([a-f0-9]{64})$/', $header, $m)
|| abs(time() - (int) $m[1]) > 300
|| !hash_equals(hash_hmac('sha256', $m[1] . '.' . $rawBody, $secret), $m[2])) {
http_response_code(400);
exit;
}
$event = json_decode($rawBody, true);
http_response_code(204);
error_log($event['type'] . ' ' . $event['id']);
OJO
<?php
$secret = getenv('BLIXYE_WEBHOOK_SECRET');
$rawBody = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_BLIXYE_SIGNATURE'] ?? '';
if (!preg_match('/^t=([0-9]+),v1=([a-f0-9]{64})$/', $header, $m)
|| abs(time() - (int) $m[1]) > 300
|| !hash_equals(hash_hmac('sha256', $m[1] . '.' . $rawBody, $secret), $m[2])) {
http_response_code(400);
exit;
}
$event = json_decode($rawBody, true);
http_response_code(204);
error_log($event['type'] . ' ' . $event['id']);
Contesta rápido con un 2xx y haz el trabajo después: Blixye espera 5 segundos como mucho.
Contesta rápido con un 2xx y haz el trabajo después: Blixye espera 5 segundos como mucho.
OJO
Contesta rápido con un 2xx y haz el trabajo después: Blixye espera 5 segundos como mucho.
Reintentos y apagado
Reintentos y apagado
OJO
Reintentos y apagado
OJO
Las entregas salen en una pasada que corre cada minuto: un evento llega en cuestión de un minuto, no al instante.
Un 2xx es una entrega hecha. Cualquier otra cosa (un 3xx, un 4xx, un 5xx, un timeout o un fallo de red) es un fallo.
Una entrega fallida se reintenta a los 1 min, 5 min, 30 min, 2 h, 6 h, 12 h y 24 h: 8 intentos en total. Después se da por perdida.
10 fallos SEGUIDOS del mismo destino, de cualquier entrega, lo apagan: queda con disabled_reason too_many_failures y sus entregas pendientes se dan por perdidas. Un solo éxito pone la cuenta a cero.
Para volver a encenderlo: PATCH /v1/webhooks/{id} con {"is_active": true}. Empieza de cero, pero lo que se perdió mientras estaba apagado no se vuelve a mandar: recupéralo con GET /v1/events.
No se garantiza el orden: un reintento puede llegar después de eventos más nuevos. Ordena por created_at.
El historial de entregas se guarda 30 días.
Al rotar el secreto (POST /v1/webhooks/{id}/rotate-secret), el viejo deja de firmar al momento: todas las entregas siguientes, reintentos incluidos, van con el nuevo. Cámbialo en tu receptor enseguida.
Al rotar el secreto (POST /v1/webhooks/{id}/rotate-secret), el viejo deja de firmar al momento: todas las entregas siguientes, reintentos incluidos, van con el nuevo. Cámbialo en tu receptor enseguida.
OJO
Al rotar el secreto (POST /v1/webhooks/{id}/rotate-secret), el viejo deja de firmar al momento: todas las entregas siguientes, reintentos incluidos, van con el nuevo. Cámbialo en tu receptor enseguida.
Si no puedes recibir webhooks (detrás de un NAT, o un script que corre de vez en cuando), sondea GET /v1/events: son los mismos eventos (API v1: eventos).
Si no puedes recibir webhooks (detrás de un NAT, o un script que corre de vez en cuando), sondea GET /v1/events: son los mismos eventos (API v1: eventos).
OJO
Si no puedes recibir webhooks (detrás de un NAT, o un script que corre de vez en cuando), sondea GET /v1/events: son los mismos eventos (API v1: eventos).