Webhooks salientes
Webhooks
Un webhook envía un mensaje a otro sistema cada vez que ocurre algo en su estudio, como una reserva nueva. La persona que lleva su web puede usarlo para actualizar una hoja de cálculo, publicar en un canal de Slack o mantener al día otra herramienta. Solo lo necesita si alguien conecta otro sistema a mano. Si usa Zapier, crea sus propios webhooks por usted. FullClass envía cada mensaje como JSON firmado a la dirección web que usted elija, así que su sistema no tiene que consultar la API una y otra vez.
Configúrelos en Configuración → Webhooks. Solo el
propietario puede abrir esta página. Añada un endpoint (la dirección que
recibe los mensajes) con un nombre, una dirección https://
accesible desde Internet y los eventos que quiere enviarle. Puede añadir
hasta 10 endpoints, y cada uno puede recibir un conjunto de eventos
distinto.
Un ajuste de ese formulario está desactivado salvo que usted lo active.
Incluir el código de la tarjeta regalo exige pensarlo bien.
Cuando alguien compra una tarjeta regalo o un bono de clases, el evento
payment.succeeded siempre indica que hubo una venta, quién
compró, quién tiene la tarjeta y por cuánto. El código de la
tarjeta es distinto, porque cualquiera que lo tenga puede gastar la tarjeta
al pagar, igual que el comprador. Actívelo solo para un endpoint en el que
confíe para guardarlo bien, y solo si su sistema necesita de verdad el
código, por ejemplo para enviarlo usted mismo por correo. Si lo vuelve a
desactivar, deja de enviarse al momento.
| Evento | Se envía cuando |
|---|---|
booking.created |
Se confirma una reserva. Una reserva sin pagar que nunca se completa
no lo envía. Un curso envía uno por sesión, y todas las sesiones de
la misma compra llevan el mismo course_purchase_id.
|
booking.updated |
Una reserva se mueve a otra clase o se edita. El mensaje indica qué ha cambiado. |
booking.canceled |
Se cancela una reserva, por usted o por el cliente. |
event.created |
Se añade una clase a su horario. Una serie repetida envía uno por fecha, y un curso envía uno por sesión. |
event.updated |
Usted cambia la hora, la ubicación, el instructor, el precio o el tamaño de una clase. El mensaje indica qué ha cambiado. Un cambio de hora incluye también la hora anterior. |
event.canceled |
Usted cancela una clase entera. El mensaje indica cuántas reservas se cancelaron, para que pueda distinguirlo de clientes que cancelan uno por uno. |
customer.created |
Existe una cuenta de cliente por primera vez, tanto si reservó, se registró por su cuenta o usted lo añadió. |
payment.succeeded |
Se cobra un pago, incluidos los pagos que usted registra a mano. La venta de una tarjeta regalo o de un bono de clases también indica la tarjeta, su tipo y la persona que la tiene. Así puede enviar un correo de bienvenida o de recompensa sin consultar nada. |
payment.refunded |
Se devuelve dinero a un cliente. Incluye una cancelación con reembolso, un reembolso que usted hace en Stripe o Square, y un contracargo que pierde. |
waitlist.offered |
Una plaza libre en una clase o un curso se ofrece a la siguiente persona de su lista de espera. |
Los mensajes de reserva y de pago incluyen el nombre y la dirección de correo del cliente. Así puede escribir a alguien o añadirlo a una lista sin buscarlo antes. Si la ficha del cliente se ha eliminado desde entonces, esos datos se omiten, pero el mensaje se envía igualmente.
Al añadir un endpoint, FullClass muestra una sola vez su clave de firma. Cada mensaje se firma con ella, para que su sistema pueda comprobar que el mensaje viene de verdad de FullClass. Cópiela directamente en la configuración de su endpoint. Si la pierde, use Renovar clave en la página del endpoint para obtener una nueva. La clave antigua deja de funcionar en cuanto aparece la nueva, así que actualice su endpoint enseguida.
Comprobar la firma
Esta parte es para la persona que lleva su web. Muestra cómo su código
comprueba que un mensaje viene de FullClass. La clave de firma no es una
contraseña, y FullClass nunca la envía. No la busque en la petición ni la
compare con nada. En su lugar, cada mensaje lleva una cabecera
FullClass-Signature. Su código la vuelve a calcular a partir de
la clave y del cuerpo del mensaje:
FullClass-Signature: t=1767225600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
-
tes el momento en que se envió el mensaje, en segundos Unix.v1es un HMAC-SHA256, escrito en hexadecimal en minúsculas. -
El HMAC se calcula sobre la marca de tiempo, un punto y el cuerpo de la
petición sin procesar, como en
<t>.<body>. -
La clave es la clave completa tal como se muestra, con el prefijo
whsec_incluido, usada como texto plano. No la decodifique de base64. -
Compare su resultado con
v1mediante una comparación de tiempo constante. Rechace el mensaje sitse aleja más de 5 minutos de su propio reloj, en cualquier dirección.
En Node.js con Express:
const crypto = require("crypto");
const express = require("express");
const secret = process.env.FULLCLASS_WEBHOOK_SECRET; // "whsec_..."
const app = express();
// express.raw keeps the exact bytes FullClass signed.
app.post("/fullclass", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req.get("FullClass-Signature") || "", req.body)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body);
// ... handle event ...
res.sendStatus(200);
});
function verify(header, rawBody) {
if (!Buffer.isBuffer(rawBody)) return false; // a JSON parser ran first
const parts = Object.fromEntries(
header.split(",").map((p) => p.trim().split("=", 2))
);
const t = Number(parts.t);
if (!Number.isInteger(t) || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(parts.t + ".")
.update(rawBody)
.digest();
const given = Buffer.from(parts.v1, "hex");
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}
Monte esta ruta antes de cualquier app.use(express.json()). Un
analizador de JSON que se ejecute primero sustituye los bytes originales por
un objeto ya analizado, y entonces la firma nunca podrá coincidir. Trate la
falta de la cabecera como una comprobación fallida. Nunca escriba la clave
ni la cabecera en sus registros.
En Python con Flask:
import hashlib, hmac, os, time
from flask import Flask, abort, request
SECRET = os.environ["FULLCLASS_WEBHOOK_SECRET"] # "whsec_..."
app = Flask(__name__)
@app.post("/fullclass")
def fullclass():
raw = request.get_data() # the exact bytes FullClass signed
if not verify(request.headers.get("FullClass-Signature", ""), raw):
abort(400)
event = request.get_json()
# ... handle event ...
return "", 200
def verify(header: str, raw_body: bytes) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
t, v1 = parts.get("t", ""), parts.get("v1", "")
if not t.isdigit() or not v1:
return False
if abs(time.time() - int(t)) > 300:
return False
expected = hmac.new(
SECRET.encode(), t.encode() + b"." + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, v1)
Cada mensaje lleva además FullClass-Event-Id,
FullClass-Event-Type y FullClass-Delivery-Id. Un
reintento se firma de nuevo con una marca de tiempo nueva, así que su firma
es distinta de la del primer intento. Use FullClass-Event-Id
para detectar un evento que ya haya procesado. Los endpoints que una
aplicación como Zapier añade por usted también van firmados, pero su clave
nunca se muestra. Así que solo puede comprobar la firma en los endpoints que
añada usted mismo.
La página de cada endpoint muestra sus envíos recientes. Ve el evento, cuándo se envió, qué respondió su servidor y cuántos intentos se hicieron. Un envío fallido se vuelve a intentar solo, con esperas cada vez más largas. También puede abrir el mensaje de un envío, o enviarlo de nuevo cuando haya arreglado su endpoint. Un endpoint que falla una y otra vez se pone en pausa solo, para que un servidor caído no acumule mensajes pendientes. Arregle el lado que recibe, y después edite el endpoint y vuelva a activarlo.