Vai al contenuto

Webhook

Per chi integra. Come Memberfy avvisa il tuo sistema quando succede qualcosa nella community, il formato della busta, la firma Standard Webhooks e come verificarla in Node, deduplicazione, ordine, nuovi tentativi, disattivazione automatica e i limiti.

Un webhook è Memberfy che chiama il tuo sistema: quando qualcuno entra, acquista, si abbona, si iscrive a un evento o completa un corso, Memberfy invia subito un POST firmato all'URL che hai registrato. È il percorso inverso dell'API, in cui è il tuo sistema a chiedere.

Il webhook lo crea il proprietario o un amministratore della community, in Impostazioni › Integrazioni e API, scheda Webhook. Vedi Integrazioni e API › Webhook. Questa guida è per chi scrive il codice che lo riceve.

Cosa arriva

Un POST per evento e per webhook, con Content-Type: application/json; charset=utf-8 e User-Agent: Memberfy-Webhooks/1. Il corpo è sempre la stessa busta:

{
  "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": "Piano Pro", "type": "SUBSCRIPTION" },
    "amount": { "amount": 9990, "currency": "BRL" }
  }
}
CampoCos'è
idL'identificativo dell'evento. È lo stesso dell'intestazione webhook-id, in tutti i tentativi e nel reinvio
typeL'evento (member.joined, purchase.completed…) o ping, il test
versionLa versione del formato di data di quell'evento. Oggi, 1 per tutti
occurredAtQuando il fatto è avvenuto in Memberfy (ISO 8601, UTC), non quando è partita la consegna
communityLa community: id e slug
dataCosa è successo. Cambia per evento; vedi Eventi dei webhook

Gli importi arrivano in centesimi, come { "amount": 9990, "currency": "BRL" }, sempre quanto ha pagato la persona. La persona coinvolta arriva in data.member, con id, name, email, phone (in formato E.164, come +5541999998888, oppure null) e phoneCountry (il paese del telefono in ISO 3166-1 alfa-2, come BR, oppure null).

Gli eventi

Sono 97 eventi in 27 famiglie, più il ping:

FamigliaEventi
membermember.joined (anche per chi viene importato da un foglio di calcolo, 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 le risposte dell'iscrizione in 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 (senza il testo del messaggio)
badgebadge.awarded, badge.revoked
(test)ping, inviato dal pulsante Invia test e alla riattivazione, con data: { "message": "…" }

Il webhook si iscrive agli eventi uno per uno o all'intera famiglia (member.*); chi seleziona la famiglia riceve anche gli eventi che vi entreranno in seguito. I campi di ognuno sono in Eventi dei webhook.

Un invito non ancora accettato non genera member.joined.

Livelli e punti

member.level_changed parte quando cambia il livello della 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 (salito, subito) o down (sceso).
  • cause: points (i punti hanno cambiato il livello) o activity (i requisiti della settimana: soddisfatti, in salita, o non soddisfatti nella settimana precedente, nella discesa del lunedì alle 00:00 nel fuso orario della community).
  • from e to: il livello prima e dopo, con id, level (il numero, 1 è il primo) e name. L'id è null finché la community usa i livelli predefiniti senza averli mai aperti.
  • Saltare più livelli insieme genera un solo evento, con il livello finale in to.

member.points_changed parte quando cambia il totale dei punti della persona nella community: from, to, delta e source (action, un'azione della persona; reversal, punti restituiti perché l'azione è stata annullata; team, una correzione del team; workflow, una correzione di un workflow).

Nessuno dei due parte con un ricalcolo dei punti, un'importazione, quando la community cambia i livelli o con la gamification disattivata.

Cosa non viene mai inviato

Nessun evento porta commissioni, importo netto, dati del processore di pagamento o della carta, codice fiscale dell'anagrafica, indirizzo, data di nascita, campi personalizzati, password, token o segreti, IP, né il testo di post, commenti e ticket. Le risposte a un modulo (form.submitted) e alle domande dell'iscrizione di un evento (event.registered) vanno come la persona le ha compilate, compreso il campo Documento. Nome, e-mail e telefono della persona (se l'ha registrato) vanno sempre: per questo la schermata avvisa, alla creazione del webhook, che i dati personali vanno alla destinazione.

Verificare la firma

Ogni consegna è firmata secondo lo standard Standard Webhooks, con il segreto del webhook (inizia con whsec_ e appare una sola volta, alla creazione o alla rotazione). Tre intestazioni:

IntestazioneCos'è
webhook-idL'id dell'evento (lo stesso id del corpo)
webhook-timestampQuando è partito il tentativo, in secondi dal 1970
webhook-signaturev1, seguito dall'HMAC-SHA256, in base64, di {webhook-id}.{webhook-timestamp}.{corpo}

Verifica sempre prima di fidarti del contenuto, con il corpo esattamente come è arrivato (senza farne prima il parse), e rifiuta un webhook-timestamp che differisca di più di 5 minuti dal tuo orologio. La libreria standardwebhooks fa entrambe le cose. In Node, con Express:

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

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

// Il corpo grezzo: la firma riguarda i byte arrivati.
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); // deduplicare
  res.sendStatus(200); // rispondi subito ed elabora dopo
  await queue.add(event); // es.: la tua coda di lavoro
});

app.listen(3000);

verify controlla la firma e l'orario e restituisce la busta già letta. Esistono librerie dello stesso standard per Python, Go, Ruby, PHP, Java e altri linguaggi.

Quando si ruota il segreto, il nuovo vale subito e il vecchio smette di valere: aggiorna subito il tuo lato.

Rispondere ed elaborare

  • Rispondi con qualsiasi 2xx entro 10 secondi. Oltre, il tentativo conta come errore.
  • Elabora dopo aver risposto (una coda, un job): un'integrazione lenta non può trattenere la risposta.
  • Deduplica tramite il webhook-id. Lo stesso evento può arrivare più di una volta (una risposta persa in rete, un reinvio dalla schermata): salva gli id già elaborati e ignora il doppione.
  • L'ordine non è garantito. Un subscription.renewed può arrivare prima del subscription.created che era in nuovo tentativo. Usa occurredAt per ordinare e, quando ti serve lo stato attuale, chiedi all'API.
  • I reindirizzamenti non vengono seguiti: un 301 o 302 conta come errore. Registra l'URL finale.

Nuovi tentativi

Senza un 2xx, Memberfy riprova. Sono fino a 8 tentativi per consegna: il primo subito e gli altri dopo 1 min, 5 min, 30 min, 2 h, 6 h, 12 h e 24 h, circa 45 ore in tutto. Esauriti gli 8, la consegna resta Non riuscita nella cronologia, e si può reinviare dalla schermata (un nuovo ciclo, con lo stesso webhook-id).

La rispostaCosa succede
2xxConsegnata
410 GoneIl webhook viene disattivato subito, senza altri tentativi, e proprietario e amministratori ricevono un'e-mail
429Riprova dopo il Retry-After (in secondi o come data), se presente; non conta come errore
Qualsiasi altro stato, tempo scaduto o errore di reteErrore; segue il calendario sopra

Disattivazione automatica

Se la destinazione resta 72 ore senza nessuna consegna riuscita (con almeno 5 errori nel periodo), il webhook viene disattivato (Disattivato per errori), i tentativi in attesa vengono annullati e proprietario e amministratori ricevono un'e-mail. Gli eventi che avvengono mentre è disattivato non vengono inviati dopo.

Per tornare, usa Riattiva nella schermata: Memberfy invia prima un ping, e il webhook torna attivo solo se la destinazione risponde 2xx.

Indirizzo e rete

  • Solo https, sulla porta 443, su un indirizzo pubblico. Gli indirizzi interni, di rete privata o di loopback vengono rifiutati.
  • Nessun reindirizzamento e un limite di 10 secondi per tentativo. Della risposta, Memberfy conserva solo l'inizio (1 KB), per la cronologia.
  • Se il tuo firewall accetta solo indirizzi noti, seleziona Invia da IP fissi sul webhook e consenti gli IP fissi di Memberfy.

Limiti

LimiteValore
Webhook per community10
Creare, inviare test, riattivare, reinviare e cambiare l'URL10 al minuto per community, sommati; oltre, 429 con Retry-After
Cronologia delle consegne e delle modifiche30 giorni

Ogni consegna riuscita di un evento conta in Webhook consegnati, nel blocco Utilizzo di Integrazioni e API. Il ping non conta.

Gestire tramite API

La schermata usa l'API dei webhook su https://developers.memberfy.net (/api/webhooks: elencare, creare, modificare URL, descrizione, eventi e IP fissi, inviare test, ruotare, disattivare, riattivare, eliminare, consegne, reinvio e cronologia). Accetta solo il token di login (Authorization: Bearer) con l'X-CommunityId, di proprietario o amministratore; una chiave API non entra. La specifica è su developers.memberfy.net/docs.json. Il catalogo degli eventi è pubblico: GET /api/integrations/events.

Modificare un webhook

PATCH /api/webhooks/{id} solo con ciò che cambia; un campo assente resta com'è.

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}'
CampoTipoNota
urltesto (fino a 2048)Stesso controllo della creazione (https, porta 443, indirizzo pubblico); conta nel limite di 10 al minuto per community
descriptiontesto (fino a 200) o null
eventslista (da 1 a 50)Tipi o famiglie intere (member.*)
fixedIpsbooleanoAttiva o disattiva Invia da IP fissi
  • La nuova configurazione vale per gli eventi che accadono dopo. Le consegne in sospeso, in nuovo tentativo o reinviate dopo restano come sono state create: stesso URL, stesso uso dell'IP fisso e stesso payload. La firma usa sempre il segreto attuale.
  • Cambiare l'url azzera failureCount.
  • Modificare non riattiva: un webhook DISABLED o DISABLED_FAILING resta disattivato. Riattivare è POST /api/webhooks/{id}/enable, che invia il test e riattiva solo con risposta 2xx.
  • Ogni modifica che cambia qualcosa diventa una riga updated nella cronologia, con il prima e il dopo di ogni campo in changes (URL mascherati). Una modifica senza cambiamenti non genera righe.
  • Risposte: 200 con il webhook, 404, 422 (campo non valido, in errors[].param) e 429.

Integrazioni pronte

Le integrazioni pronte (Discord, Slack, Telegram, RD Station, HubSpot) usano la stessa API, in /api/apps: GET /api/apps (catalogo, con l'installazione di ogni app), PUT /api/apps/{slug}/installation (attiva o salva la configurazione e gli eventi), POST …/installation/test, DELETE …/installation (disattiva; con ?remove=true, rimuove e cancella la configurazione e le consegne), POST /api/apps/telegram/chats (Trova il mio gruppo) e le consegne in …/installation/deliveries, con reinvio. Stesse regole di accesso: token di accesso di proprietario o admin, mai una chiave API. I campi segreti tornano mascherati; in un PUT, un campo segreto assente mantiene il valore salvato.

Correlati