Saltar al contenido

Webhooks

Para quien integra. Cómo Memberfy avisa a tu sistema cuando algo ocurre en la comunidad, el formato del sobre, la firma Standard Webhooks y cómo verificarla en Node, deduplicación, orden, reintentos, desactivación automática y los límites.

Un webhook es Memberfy llamando a tu sistema: cuando alguien entra, compra, se suscribe, se inscribe en un evento o termina un curso, Memberfy envía un POST a la URL que registraste, firmado, en el acto. Es el camino inverso de la API, en la que es tu sistema quien pregunta.

El webhook lo crea el propietario o un administrador de la comunidad, en Configuración › Integraciones y APIs, pestaña Webhooks. Ver Integraciones y APIs › Webhooks. Esta guía es para quien escribe el código que lo recibe.

Qué llega

Un POST por evento y por webhook, con Content-Type: application/json; charset=utf-8 y User-Agent: Memberfy-Webhooks/1. El cuerpo es siempre el mismo sobre:

{
  "id": "evt_01JAB3K7Q9X2M4N6P8R0S2T4V6",
  "type": "purchase.completed",
  "version": 1,
  "occurredAt": "2026-10-19T14:03:11.402Z",
  "community": { "id": "c3f1a2b4-5d6e-4f70-8a9b-0c1d2e3f4a5b", "slug": "apicon" },
  "data": {
    "member": { "id": "9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d", "name": "Maria Souza", "email": "[email protected]", "phone": "+5541999998888", "phoneCountry": "BR" },
    "purchaseId": "…",
    "product": { "id": "…", "name": "Plan Pro", "type": "SUBSCRIPTION" },
    "amount": { "amount": 9990, "currency": "BRL" }
  }
}
CampoQué es
idEl identificador del evento. Es el mismo de la cabecera webhook-id, en todos los intentos y en el reenvío
typeEl evento (member.joined, purchase.completed…) o ping, la prueba
versionLa versión del formato de data de ese evento. Hoy, 1 en todos
occurredAtCuándo ocurrió el hecho en Memberfy (ISO 8601, UTC), no cuándo salió la entrega
communityLa comunidad: id y slug
dataLo que ocurrió. Cambia por evento; ver Eventos de webhook

Los importes vienen en céntimos, como { "amount": 9990, "currency": "BRL" }, siempre lo que pagó la persona. La persona implicada viene en data.member, con id, name, email, phone (en formato E.164, como +5541999998888, o null) y phoneCountry (el país del teléfono en ISO 3166-1 alfa-2, como BR, o null).

Los eventos

Son 97 eventos en 27 familias, más el ping:

FamiliaEventos
membermember.joined (también de quien se importa desde una hoja de cálculo, con source: "import"), member.left, member.returned, member.role_changed, member.tag_added, member.tag_removed, member.group_joined, member.group_left, member.field_changed, member.level_changed, member.points_changed
purchasepurchase.completed, purchase.refunded
subscriptionsubscription.created, subscription.renewed, subscription.canceled, subscription.ended, subscription.plan_changed, subscription.past_due, subscription.trial_ending
eventevent.registered (con las respuestas de la inscripción en registration), event.checked_in, event.check_in_undone, event.ticket_assigned, event.registration_canceled, event.waitlist_offered, event.created, event.updated, event.deleted, event.stream_started, event.recording_published, event.rescheduled
coursecourse.enrolled, course.completed, course.published, course.updated, course.left
certificatecertificate.issued, certificate.revoked
formform.submitted, form.submission_reviewed, form.published, form.closed, form.reopened
lessonlesson.completed, lesson.rated, lesson.published
commentcomment.created, comment.deleted
reactionreaction.added, reaction.removed
postpost.created, post.updated, post.moved, post.deleted
checkoutcheckout.started, checkout.abandoned, checkout.recovered
spacespace.first_visit, space.access_granted, space.created, space.updated, space.deleted
quizquiz.passed, quiz.failed
couponcoupon.redeemed
surveysurvey.answered
proposalproposal.submitted, proposal.decided
upvoteupvote.topic_created, upvote.topic_status_changed
downloaddownload.completed, download.item_published, download.item_updated, download.item_moved, download.file_added, download.file_replaced, download.file_removed, download.item_deleted, download.category_created
classifiedclassified.published, classified.interest_received
parcelparcel.received, parcel.picked_up
visitorvisitor.invited, visitor.arrived
attendanceattendance.checked_in, attendance.checked_out
support_ticketsupport_ticket.created, support_ticket.resolved
bookingbooking.created, booking.canceled, booking.checked_in, booking.no_show
messagemessage.received (sin el texto del mensaje)
badgebadge.awarded, badge.revoked
(prueba)ping, enviado por el botón Enviar prueba y al reactivar, con data: { "message": "…" }

El webhook se suscribe a eventos uno a uno o a la familia entera (member.*); quien marca la familia recibe también los eventos que entren en ella después. Los campos de cada uno están en Eventos de webhook.

Una invitación aún no aceptada no genera member.joined.

Niveles y puntos

member.level_changed sale cuando cambia el nivel de la persona:

{
  "member": { "id": "…", "name": "Maria Souza", "email": "[email protected]" },
  "direction": "down",
  "cause": "activity",
  "from": { "id": "…", "level": 5, "name": "Kernel" },
  "to": { "id": "…", "level": 4, "name": "Shell" },
  "points": 640
}
  • direction: up (subió, al instante) o down (bajó).
  • cause: points (los puntos cambiaron el nivel) o activity (los requisitos de la semana: cumplidos, al subir, o no cumplidos la semana anterior, en la bajada del lunes a las 00:00 en la zona horaria de la comunidad).
  • from y to: el nivel antes y después, con id, level (el número, 1 es el primero) y name. El id viene null mientras la comunidad usa los niveles predeterminados sin haberlos abierto nunca.
  • Saltar varios niveles a la vez genera un evento, con el nivel final en to.

member.points_changed sale cuando cambia el total de puntos de la persona en la comunidad: from, to, delta y source (action, una acción de la persona; reversal, puntos devueltos porque la acción se deshizo; team, un ajuste del equipo; workflow, un ajuste de un workflow).

Ninguno de los dos sale en un recálculo de puntos, en una importación, cuando la comunidad cambia los niveles ni con la gamificación desactivada.

Lo que nunca va

Ningún evento lleva comisiones, importe neto, datos del procesador de pago o de la tarjeta, documento fiscal del registro, dirección, fecha de nacimiento, campos personalizados, contraseña, token o secreto, IP, ni el texto de publicaciones, comentarios y tickets. Las respuestas de un formulario (form.submitted) y de las preguntas de la inscripción de un evento (event.registered) van como la persona las rellenó, incluido el campo Documento. El nombre, el correo y el teléfono de la persona (si lo registró) van siempre: por eso la pantalla avisa, al crear el webhook, de que los datos personales van al destino.

Verificar la firma

Toda entrega va firmada con el estándar Standard Webhooks, con el secreto del webhook (empieza por whsec_ y aparece una sola vez, al crearlo o al rotarlo). Tres cabeceras:

CabeceraQué es
webhook-idEl id del evento (el mismo id del cuerpo)
webhook-timestampCuándo salió el intento, en segundos desde 1970
webhook-signaturev1, seguido del HMAC-SHA256, en base64, de {webhook-id}.{webhook-timestamp}.{cuerpo}

Verifica siempre antes de confiar en el contenido, con el cuerpo exactamente como llegó (sin parsearlo antes), y rechaza un webhook-timestamp con más de 5 minutos de diferencia con tu reloj. La librería standardwebhooks hace las dos cosas. En Node, con Express:

import express from 'express';
import { Webhook } from 'standardwebhooks';

const app = express();
const webhook = new Webhook(process.env.MEMBERFY_WEBHOOK_SECRET); // whsec_…

// El cuerpo crudo: la firma es sobre los bytes que llegaron.
app.post('/memberfy', express.raw({ type: 'application/json' }), async (req, res) => {
  let event;
  try {
    event = webhook.verify(req.body.toString('utf8'), {
      'webhook-id': req.get('webhook-id'),
      'webhook-timestamp': req.get('webhook-timestamp'),
      'webhook-signature': req.get('webhook-signature'),
    });
  } catch {
    return res.status(400).send('invalid signature');
  }

  if (await alreadyProcessed(event.id)) return res.sendStatus(200); // deduplicar
  res.sendStatus(200); // responde enseguida y procesa después
  await queue.add(event); // p. ej.: tu cola de trabajo
});

app.listen(3000);

verify comprueba la firma y la hora y devuelve el sobre ya leído. Hay librerías del mismo estándar para Python, Go, Ruby, PHP, Java y otros lenguajes.

Al rotar el secreto, el nuevo vale en el acto y el antiguo deja de valer: actualiza tu lado enseguida.

Responder y procesar

  • Responde con cualquier 2xx en un máximo de 10 segundos. Pasado ese tiempo, el intento cuenta como fallo.
  • Procesa después de responder (una cola, un job): una integración lenta no puede retener la respuesta.
  • Deduplica por el webhook-id. El mismo evento puede llegar más de una vez (una respuesta que se perdió en la red, un reenvío desde la pantalla): guarda los ids ya procesados e ignora el repetido.
  • El orden no está garantizado. Un subscription.renewed puede llegar antes que el subscription.created que estaba en reintento. Usa el occurredAt para ordenar y, cuando necesites el estado actual, pregunta a la API.
  • Las redirecciones no se siguen: un 301 o 302 cuenta como fallo. Registra la URL final.

Reintentos

Sin un 2xx, Memberfy lo intenta de nuevo. Son hasta 8 intentos por entrega: el primero en el acto y los demás después de 1 min, 5 min, 30 min, 2 h, 6 h, 12 h y 24 h, unas 45 horas en total. Agotados los 8, la entrega queda como Falló en el historial, y se puede reenviar desde la pantalla (un ciclo nuevo, con el mismo webhook-id).

La respuestaQué pasa
2xxEntregada
410 GoneEl webhook se desactiva en el acto, sin más intentos, y el propietario y los administradores reciben un correo
429Lo intenta de nuevo después del Retry-After (en segundos o como fecha), si lo hay; no cuenta como fallo
Cualquier otro estado, tiempo agotado o error de redFallo; sigue el calendario de arriba

Desactivación automática

Si el destino pasa 72 horas sin ninguna entrega con éxito (con al menos 5 fallos en ese periodo), el webhook se desactiva (Desactivado por fallos), los intentos pendientes se cancelan y el propietario y los administradores reciben un correo. Los eventos que ocurran mientras esté desactivado no se envían después.

Para volver, usa Reactivar en la pantalla: Memberfy envía antes un ping, y el webhook solo vuelve si el destino responde 2xx.

Dirección y red

  • Solo https, en el puerto 443, en una dirección pública. Las direcciones internas, de red privada o de loopback se rechazan.
  • Sin redirección y con un límite de 10 segundos por intento. De la respuesta, Memberfy guarda solo el comienzo (1 KB), para el historial.
  • Si tu firewall solo acepta direcciones conocidas, marca Enviar desde IPs fijas en el webhook y permite las IPs fijas de Memberfy.

Límites

LímiteValor
Webhooks por comunidad10
Crear, enviar prueba, reactivar, reenviar y cambiar la URL10 por minuto por comunidad, sumadas; por encima, 429 con Retry-After
Historial de entregas y de cambios30 días

Cada entrega con éxito de un evento cuenta en Webhooks entregados, en el bloque Uso de Integraciones y APIs. El ping no cuenta.

Gestionar por API

La pantalla usa la API de webhooks en https://developers.memberfy.net (/api/webhooks: listar, crear, editar la URL, la descripción, los eventos y las IPs fijas, enviar prueba, rotar, desactivar, reactivar, eliminar, entregas, reenvío e historial). Acepta solo el token de inicio de sesión (Authorization: Bearer) con el X-CommunityId, de propietario o administrador; una clave de API no entra. La especificación está en developers.memberfy.net/docs.json. El catálogo de eventos es público: GET /api/integrations/events.

Editar un webhook

PATCH /api/webhooks/{id} solo con lo que cambia; un campo ausente queda como está.

curl -X PATCH https://developers.memberfy.net/api/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-CommunityId: $COMMUNITY_ID" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://hooks.memberfy.net/crm-v2", "events": ["member.*", "purchase.completed"], "fixedIps": true}'
CampoTipoObservación
urltexto (hasta 2048)Misma comprobación que en la creación (https, puerto 443, dirección pública); cuenta para el límite de 10 por minuto por comunidad
descriptiontexto (hasta 200) o null
eventslista (1 a 50)Tipos o familias enteras (member.*)
fixedIpsbooleanoActiva o desactiva Enviar desde IPs fijas
  • La configuración nueva vale para los eventos que ocurran después. Las entregas pendientes, en reintento o reenviadas después siguen como se crearon: misma URL, mismo uso de IP fija y mismo payload. La firma usa siempre el secreto actual.
  • Cambiar la url pone a cero failureCount.
  • Editar no reactiva: un webhook DISABLED o DISABLED_FAILING sigue desactivado. Reactivar es POST /api/webhooks/{id}/enable, que envía la prueba y solo reactiva con respuesta 2xx.
  • Cada edición que cambia algo se convierte en una línea updated del historial, con el antes y el después de cada campo en changes (URLs enmascaradas). Una edición sin cambios no genera línea.
  • Respuestas: 200 con el webhook, 404, 422 (campo inválido, en errors[].param) y 429.

Integraciones listas

Las integraciones listas (Discord, Slack, Telegram, RD Station, HubSpot) usan la misma API, en /api/apps: GET /api/apps (catálogo, con la instalación de cada app), PUT /api/apps/{slug}/installation (activa o guarda la configuración y los eventos), POST …/installation/test, DELETE …/installation (desactiva; con ?remove=true, elimina y borra la configuración y las entregas), POST /api/apps/telegram/chats (Encontrar mi grupo) y las entregas en …/installation/deliveries, con reenvío. Las mismas reglas de acceso: token de inicio de sesión de propietario o administrador, nunca clave de API. Los campos secretos vuelven enmascarados; en un PUT, un campo secreto ausente mantiene el valor guardado.

Relacionados