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,sourcespor 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.dataincluyeapplicationId,programSlug,affiliateName,scoreyautoApproved.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.dataincluyepartnershipId,programSlug,affiliateNameyorigin.
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.
- Lea el cuerpo CRUDO tal como llegó. Un objeto reserializado cambia los bytes y la firma deja de coincidir.
- Rechace una marca de tiempo a más de cinco minutos de su reloj: es la ventana contra repeticiones.
- 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. - Guarde el
webhook-idprocesado: un reintento lleva el mismo.
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);
});
}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 eventowebhook.testfirmado 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:
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
De la clave de API al prospecto inscrito: busque socios, preseleccione, encuentre correos con coste confirmado (402), reintente con idempotencia y respete los límites.
Contacte a los creadores seleccionados desde su propio Gmail, deje que los seguimientos se envíen solos y siga a cada persona de Encontrado a Socio activo.
Conecte su asistente de IA a GetAff (MCP, OAuth): busque, clasifique y contacte creadores en lenguaje natural, con cada coste y cada envío confirmados por usted.