GetAff
Artículo

Cómo conectar GetAff a su CRM o a Slack con webhooks firmados

La respuesta de un creador merece atención en la misma hora, no en la próxima revisión semanal. Los webhooks permiten que GetAff avise a sus sistemas en cuanto ocurre algo — una búsqueda terminada, un prospecto que responde, un creador que presenta su solicitud, una colaboración aprobada—, de modo que su CRM recibe la actualización y su equipo el mensaje de Slack sin que nadie tenga que consultar.

Este artículo explica los cuatro eventos, el registro de un punto de conexión, la verificación de la firma en Node y en Python, el calendario de reintentos y dos integraciones concretas.

Actualizado el

Los cuatro eventos

Cada entrega es un cuerpo JSON { id, type, created_at, data }. data lleva identificadores y recuentos, nunca una dirección de correo ni un mensaje: cuando necesite el objeto completo, léalo a través de la API con el identificador.

  • search.finished — terminó una búsqueda de socios de uno de sus sitios, tanto si se lanzó desde la pantalla como desde el chat, la API o MCP. data: siteId, searchId, results, onTopic, sources por fuente, startedAt, finishedAt.
  • prospect.replied — un prospecto respondió a una secuencia de contacto, detectado en su buzón por el ciclo de diez minutos. data: prospectId, sequenceId, mailboxEmail, repliedAt. Una etapa movida a mano no lo dispara.
  • application.received — un creador solicitó unirse a uno de sus programas. data incluye applicationId, programSlug, affiliateName, score y autoApproved.
  • partner.approved — una colaboración pasó a aprobada: por su decisión, por su regla de aprobación automática o por una invitación aceptada. data incluye partnershipId, programSlug, affiliateName y origin.

Registrar un punto de conexión

Desde Ajustes de la cuenta › API › Webhooks, o con una clave que tenga el ámbito write:

curl -s -X POST https://www.getaff.org/api/v1/webhooks \
  -H "Authorization: Bearer $GETAFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://hooks.example.com/getaff",
       "events": ["prospect.replied", "application.received"],
       "description": "CRM sync"}'

La respuesta incluye el secreto de firma (whsec_…) una sola vez: guárdelo en el servidor junto a sus demás secretos. Una cuenta admite hasta diez puntos de conexión, cada uno con sus propios eventos y su propio secreto. La URL debe ser https:// en un host público; las direcciones privadas, de bucle local y de enlace local se rechazan al registrar y de nuevo en cada entrega.

Verifique cada entrega

Las entregas siguen la especificación Standard Webhooks. Tres cabeceras acompañan al cuerpo: webhook-id, webhook-timestamp (segundos Unix) y webhook-signature — v1, seguido del HMAC-SHA256 en base64 de {id}.{timestamp}.{body}, con los bytes detrás de whsec_ como clave. Cualquier biblioteca Standard Webhooks la verifica solo con el secreto; sin biblioteca, bastan una docena de líneas.

  1. Lea el cuerpo CRUDO tal como llegó. Un objeto reserializado cambia los bytes y la firma deja de coincidir.
  2. Rechace una marca de tiempo a más de cinco minutos de su reloj: es la ventana contra repeticiones.
  3. Calcule el HMAC y compárelo en tiempo constante con cada valor v1,… separado por espacios. Basta una coincidencia: durante una rotación de secreto viajan dos valores.
  4. Guarde el webhook-id procesado: un reintento lleva el mismo.
Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(rawBody, headers, secret) {
  const id = headers['webhook-id'];
  const ts = Number(headers['webhook-timestamp']);
  const sigs = (headers['webhook-signature'] ?? '').split(' ');
  if (!id || !ts || Math.abs(Date.now() / 1000 - ts) > 300) return false;
  const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
  const expected = Buffer.from(
    'v1,' + createHmac('sha256', key).update(`${id}.${ts}.${rawBody}`).digest('base64'),
  );
  return sigs.some((s) => {
    const got = Buffer.from(s);
    return got.length === expected.length && timingSafeEqual(got, expected);
  });
}
Python
import base64, hashlib, hmac, time

def verify(raw_body: bytes, headers, secret: str) -> bool:
    msg_id = headers.get("webhook-id", "")
    ts = headers.get("webhook-timestamp", "")
    if not msg_id or not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{ts}.".encode() + raw_body
    expected = "v1," + base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    return any(hmac.compare_digest(s, expected)
               for s in headers.get("webhook-signature", "").split(" "))

Una petición sin firma o mal firmada a su punto de conexión no vino de GetAff. Las entregas nunca llevan su clave de API.

Responda rápido y cuente con los reintentos

Responda cualquier 2xx en menos de diez segundos; el cuerpo de su respuesta se ignora. Todo lo demás — otro estado, un tiempo agotado, un error de conexión — es un intento fallido, que se repite tras 1 minuto, 5 minutos, 30 minutos, 2 horas y 12 horas: seis intentos en unas quince horas, siempre con el mismo webhook-id y el mismo cuerpo. Tras el sexto, la entrega pasa a fallida definitiva: conservada y visible en Ajustes de la cuenta o mediante GET /v1/webhooks/{id}, nunca reintentada. Un 404 o un 410 de su parte es definitivo de inmediato.

  • Primero confirme la recepción y después trabaje: envíe el evento a una cola y devuelva 200.
  • Elimine duplicados por webhook-id, para que un reintento tras una respuesta lenta se procese una sola vez.
  • Use la acción «Probar», o POST /v1/webhooks/{id}/test, para enviar un evento webhook.test firmado y ver la respuesta de su punto de conexión.

Ejemplo: mantener su CRM sincronizado

Suscriba un punto de conexión a prospect.replied y partner.approved. Ante una respuesta, obtenga el prospecto con GET /v1/prospects/{prospectId} (estado, canal de contacto, procedencia), cree o actualice el contacto en su CRM y abra una tarea para el responsable de la cuenta. Ante una aprobación, pase la oportunidad a «socio» y guarde el partnershipId, para que finanzas pueda asociarle después las comisiones.

Como data contiene identificadores y no datos personales, el propio evento puede registrarse sin riesgo; los detalles solo viajan a través de su llamada autenticada a la API.

Ejemplo: un mensaje de Slack por cada solicitud

Los webhooks entrantes de Slack aceptan un simple POST en JSON. Tras verificar la firma, reenvíe un breve resumen de application.received:

Reenviar a Slack
if (event.type === 'application.received') {
  const d = event.data;
  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: `New application to ${d.programName} from ${d.affiliateName} (score ${d.score})`,
    }),
  });
}

search.finished también sirve para un buen resumen: publique el número de resultados pertinentes y un enlace a la página «Encontrar» del sitio, para que el equipo revise una búsqueda nueva el mismo día.

Un punto de conexión por tarea

Cada punto de conexión tiene sus propios eventos y su propio secreto: sepárelos por uso en lugar de enviarlo todo a una sola URL. Una sincronización con el CRM que solo necesita prospect.replied y partner.approved no tiene por qué recibir search.finished; un puente hacia Slack para el equipo de partners puede limitarse a application.received. Los manejadores pequeños son más fáciles de verificar, de supervisar y de pausar.

Cada punto de conexión muestra además el estado de su última entrega y sus veinte últimos intentos, con la respuesta que dio su servidor. Cuando un receptor falla, ese historial indica si el problema está en su código, su certificado o su cortafuegos, y la entrega fallida definitiva conserva durante treinta días cada evento perdido: nada se pierde mientras lo corrige.

Rotar un secreto sin interrupciones

Rote el secreto desde Ajustes de la cuenta o con POST /v1/webhooks/{id}/rotate. Durante las 24 horas siguientes, cada entrega lleva dos firmas — la nueva y la antigua—, de modo que un receptor que actualice más tarde ese día sigue verificando. Un punto de conexión también puede pausarse sin perder su secreto; en pausa no recibe nada y no se encola nada. La documentación de webhooks recoge el contrato completo, y la referencia, cada punto de conexión.

Documentación

El contrato exacto, generado a partir de los esquemas que aplican las rutas:

Para leer después