Pular para o conteúdo

Webhooks

Para quem integra. Como a Memberfy avisa o seu sistema quando algo acontece na comunidade, o formato do envelope, a assinatura Standard Webhooks e como verificá-la em Node, deduplicação, ordem, retentativas, desligamento automático e os limites.

Um webhook é a Memberfy chamando o seu sistema: quando alguém entra, compra, assina, se inscreve num evento ou conclui um curso, a Memberfy manda um POST para a URL que você cadastrou, assinado, na hora. É o caminho inverso da API, em que é o seu sistema que pergunta.

Quem cria o webhook é o proprietário ou um administrador da comunidade, em Configurações › Integrações e APIs, aba Webhooks. Ver Integrações & APIs › Webhooks. Este guia é para quem escreve o código que recebe.

O que chega

Um POST por evento e por webhook, com Content-Type: application/json; charset=utf-8 e User-Agent: Memberfy-Webhooks/1. O corpo é sempre o mesmo envelope:

{
  "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": "Plano Pro", "type": "SUBSCRIPTION" },
    "amount": { "amount": 9990, "currency": "BRL" }
  }
}
CampoO que é
idO identificador do evento. É o mesmo do cabeçalho webhook-id, em todas as tentativas e no reenvio
typeO evento (member.joined, purchase.completed…) ou ping, o teste
versionA versão do formato de data daquele evento. Hoje, 1 em todos
occurredAtQuando o fato aconteceu na Memberfy (ISO 8601, UTC), não quando a entrega saiu
communityA comunidade: id e slug
dataO que aconteceu. Muda por evento; ver Eventos de webhook

Valores em dinheiro vêm em centavos, como { "amount": 9990, "currency": "BRL" }, sempre o que a pessoa pagou. A pessoa envolvida vem em data.member, com id, name, email, phone (no formato E.164, como +5541999998888, ou null) e phoneCountry (o país do telefone em ISO 3166-1 alfa-2, como BR, ou null).

Os eventos

São 97 eventos em 27 famílias, mais o ping:

FamíliaEventos
membermember.joined (inclusive de quem é importado por planilha, com 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 (com as respostas da inscrição em 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 (sem o texto da mensagem)
badgebadge.awarded, badge.revoked
(teste)ping, enviado pelo botão Enviar teste e ao religar, com data: { "message": "…" }

O webhook assina eventos um a um ou a família inteira (member.*); quem marca a família recebe também os eventos que entrarem nela depois. Os campos de cada um estão em Eventos de webhook.

Um convite ainda não aceito não gera member.joined.

Níveis e pontos

member.level_changed sai quando o nível da pessoa muda:

{
  "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 (subiu, na hora) ou down (desceu).
  • cause: points (os pontos mudaram o nível) ou activity (os requisitos da semana: cumpridos, na subida, ou não cumpridos na semana anterior, na queda da segunda às 00:00 no fuso da comunidade).
  • from e to: o nível antes e depois, com id, level (o número, 1 é o primeiro) e name. O id vem null enquanto a comunidade usa os níveis padrão sem nunca os ter aberto.
  • Pular vários níveis de uma vez gera um evento, com o nível final em to.

member.points_changed sai quando o total de pontos da pessoa na comunidade muda: from, to, delta e source (action, uma ação da pessoa; reversal, pontos devolvidos porque a ação foi desfeita; team, um ajuste da equipe; workflow, um ajuste de workflow).

Nenhum dos dois sai num recálculo de pontos, numa importação, quando a comunidade muda os níveis ou com a gamificação desligada.

O que nunca vai

Nenhum evento leva taxa, valor líquido, dados do processador de pagamento ou do cartão, CPF/CNPJ do cadastro, endereço, data de nascimento, campos customizados, senha, token ou segredo, IP, nem o texto de posts, comentários e chamados. As respostas de um formulário (form.submitted) e das perguntas da inscrição de um evento (event.registered) vão como a pessoa preencheu, inclusive o campo Documento. Nome, e-mail e telefone da pessoa vão sempre (o telefone, quando ela cadastrou um): por isso a tela avisa, ao criar o webhook, que dados pessoais seguem para o destino.

Verificar a assinatura

Toda entrega vai assinada no padrão Standard Webhooks, com o segredo do webhook (começa com whsec_ e aparece uma vez só, na criação ou ao rotacionar). Três cabeçalhos:

CabeçalhoO que é
webhook-idO id do evento (o mesmo id do corpo)
webhook-timestampQuando a tentativa saiu, em segundos desde 1970
webhook-signaturev1, seguido do HMAC-SHA256, em base64, de {webhook-id}.{webhook-timestamp}.{corpo}

Verifique sempre antes de confiar no conteúdo, com o corpo exatamente como chegou (sem fazer o parse antes), e recuse um webhook-timestamp com mais de 5 minutos de diferença do seu relógio. A biblioteca standardwebhooks faz as duas coisas. Em Node, com Express:

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

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

// O corpo cru: a assinatura é sobre os bytes que chegaram.
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); // responda logo e processe depois
  await queue.add(event); // ex.: a sua fila de trabalho
});

app.listen(3000);

O verify confere a assinatura e o horário e devolve o envelope já lido. Bibliotecas do mesmo padrão existem para Python, Go, Ruby, PHP, Java e outras linguagens.

Ao rotacionar o segredo, o novo vale na hora e o antigo deixa de valer: atualize o seu lado logo em seguida.

Responder e processar

  • Responda com qualquer 2xx em até 10 segundos. Passou disso, a tentativa conta como falha.
  • Processe depois de responder (uma fila, um job): uma integração lenta não pode segurar a resposta.
  • Deduplique pelo webhook-id. O mesmo evento pode chegar mais de uma vez (uma resposta que se perdeu na rede, um reenvio pela tela): guarde os ids já processados e ignore o repetido.
  • A ordem não é garantida. Um subscription.renewed pode chegar antes do subscription.created que ficou em retentativa. Use o occurredAt para ordenar e, quando precisar do estado atual, pergunte à API.
  • Redirecionamentos não são seguidos: um 301 ou 302 conta como falha. Cadastre a URL final.

Retentativas

Sem um 2xx, a Memberfy tenta de novo. São até 8 tentativas por entrega: a primeira na hora e as outras depois de 1 min, 5 min, 30 min, 2 h, 6 h, 12 h e 24 h, cerca de 45 horas no total. Esgotadas as 8, a entrega fica como Falhou no histórico, e dá para reenviar pela tela (um ciclo novo, com o mesmo webhook-id).

A respostaO que acontece
2xxEntregue
410 GoneO webhook é desligado na hora, sem mais tentativas, e o proprietário e os administradores recebem um e-mail
429Tenta de novo depois do Retry-After (em segundos ou como data), se houver; não conta como falha
Qualquer outro status, tempo esgotado ou erro de redeFalha; segue o calendário acima

Desligamento automático

Se o destino ficar 72 horas sem nenhuma entrega com sucesso (com pelo menos 5 falhas no período), o webhook é desligado (Desligado por falha), as tentativas pendentes são canceladas e o proprietário e os administradores recebem um e-mail. Os eventos que acontecerem enquanto ele estiver desligado não são enviados depois.

Para voltar, use Religar na tela: a Memberfy manda um ping antes, e o webhook só volta se o destino responder 2xx.

Endereço e rede

  • Só https, na porta 443, num endereço público. Endereços internos, de rede privada ou de loopback são recusados.
  • Sem redirecionamento e com 10 segundos de limite por tentativa. Da resposta, a Memberfy guarda só o começo (1 KB), para o histórico.
  • Se o seu firewall só aceita endereços conhecidos, marque Enviar de IPs fixos no webhook e libere os IPs fixos da Memberfy.

Limites

LimiteValor
Webhooks por comunidade10
Criar, enviar teste, religar, reenviar e trocar a URL10 por minuto por comunidade, somadas; acima disso, 429 com Retry-After
Histórico de entregas e de alterações30 dias

Cada entrega com sucesso de um evento conta em Webhooks entregues, no bloco Uso de Integrações & APIs. O ping não conta.

Gerenciar por API

A tela usa a API de webhooks em https://developers.memberfy.net (/api/webhooks: listar, criar, editar a URL, a descrição, os eventos e os IPs fixos, enviar teste, rotacionar, desligar, religar, apagar, entregas, reenvio e histórico). Ela aceita só o token de login (Authorization: Bearer) com o X-CommunityId, de proprietário ou administrador; chave de API não entra. A especificação está em developers.memberfy.net/docs.json. O catálogo de eventos é público: GET /api/integrations/events.

Editar um webhook

PATCH /api/webhooks/{id} com só o que muda; campo ausente fica 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}'
CampoTipoObservação
urltexto (até 2048)Mesma checagem da criação (https, porta 443, endereço público); entra no limite de 10 por minuto por comunidade
descriptiontexto (até 200) ou null
eventslista (1 a 50)Tipos ou famílias inteiras (member.*)
fixedIpsbooleanoLiga ou desliga Enviar de IPs fixos
  • A configuração nova vale para os eventos que acontecerem depois. Entregas pendentes, em retentativa ou reenviadas depois seguem como foram criadas: mesma URL, mesmo uso de IP fixo e mesmo payload. A assinatura usa sempre o segredo atual.
  • Trocar a url zera failureCount.
  • Editar não religa: um webhook DISABLED ou DISABLED_FAILING continua desligado. Religar é POST /api/webhooks/{id}/enable, que manda o teste e só religa com resposta 2xx.
  • Cada edição que muda algo vira uma linha updated no histórico, com o antes e o depois de cada campo em changes (URLs mascaradas). Uma edição sem mudança não gera linha.
  • Respostas: 200 com o webhook, 404, 422 (campo inválido, em errors[].param) e 429.

Integrações prontas

As integrações prontas (Discord, Slack, Telegram, RD Station, HubSpot) usam a mesma API, em /api/apps: GET /api/apps (catálogo, com a instalação de cada app), PUT /api/apps/{slug}/installation (liga ou salva a configuração e os eventos), POST …/installation/test, DELETE …/installation (desliga; com ?remove=true, remove e apaga a configuração e as entregas), POST /api/apps/telegram/chats (Encontrar meu grupo) e as entregas em …/installation/deliveries, com reenvio. As mesmas regras de acesso: token de login de proprietário ou administrador, nunca chave de API. Os campos secretos voltam mascarados; num PUT, um campo secreto ausente mantém o valor salvo.

Relacionados