Integraciones

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
  • t es el momento en que se envió el mensaje, en segundos Unix. v1 es 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 v1 mediante una comparación de tiempo constante. Rechace el mensaje si t se 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)
Compruebe el cuerpo sin procesar, antes de interpretarlo. Si su framework interpreta el JSON y usted lo vuelve a convertir en texto, los espacios y el orden de las claves cambian. Entonces la firma nunca coincidirá. Lea primero el cuerpo como bytes, compruébelo y después interprételo. Por el mismo motivo, no se salte la comprobación de la marca de tiempo. Impide que alguien que haya capturado un mensaje antiguo se lo vuelva a enviar.

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.

¿No recibe los eventos? Compruebe el estado del endpoint en Configuración → Webhooks. Si aparece en pausa tras errores repetidos, su servidor no estaba accesible o devolvía errores. Cuando vuelva a funcionar, active de nuevo el endpoint con Editar. Su contador de errores vuelve entonces a cero. Los eventos ocurridos durante la pausa no se envían ni aparecen en su lista de envíos, así que no hay nada que enviar de nuevo para ellos. «Enviar de nuevo» sigue funcionando con los envíos fallidos de antes de la pausa.