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" }
}
}
| Campo | Cos'è |
|---|---|
id | L'identificativo dell'evento. È lo stesso dell'intestazione webhook-id, in tutti i tentativi e nel reinvio |
type | L'evento (member.joined, purchase.completed…) o ping, il test |
version | La versione del formato di data di quell'evento. Oggi, 1 per tutti |
occurredAt | Quando il fatto è avvenuto in Memberfy (ISO 8601, UTC), non quando è partita la consegna |
community | La community: id e slug |
data | Cosa è 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:
| Famiglia | Eventi |
|---|---|
member | member.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 |
purchase | purchase.completed, purchase.refunded |
subscription | subscription.created, subscription.renewed, subscription.canceled, subscription.ended, subscription.plan_changed, subscription.past_due, subscription.trial_ending |
event | event.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 |
course | course.enrolled, course.completed, course.published, course.updated, course.left |
certificate | certificate.issued, certificate.revoked |
form | form.submitted, form.submission_reviewed, form.published, form.closed, form.reopened |
lesson | lesson.completed, lesson.rated, lesson.published |
comment | comment.created, comment.deleted |
reaction | reaction.added, reaction.removed |
post | post.created, post.updated, post.moved, post.deleted |
checkout | checkout.started, checkout.abandoned, checkout.recovered |
space | space.first_visit, space.access_granted, space.created, space.updated, space.deleted |
quiz | quiz.passed, quiz.failed |
coupon | coupon.redeemed |
survey | survey.answered |
proposal | proposal.submitted, proposal.decided |
upvote | upvote.topic_created, upvote.topic_status_changed |
download | download.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 |
classified | classified.published, classified.interest_received |
parcel | parcel.received, parcel.picked_up |
visitor | visitor.invited, visitor.arrived |
attendance | attendance.checked_in, attendance.checked_out |
support_ticket | support_ticket.created, support_ticket.resolved |
booking | booking.created, booking.canceled, booking.checked_in, booking.no_show |
message | message.received (senza il testo del messaggio) |
badge | badge.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) odown(sceso).cause:points(i punti hanno cambiato il livello) oactivity(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).frometo: il livello prima e dopo, conid,level(il numero, 1 è il primo) ename. L'idènullfinché 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:
| Intestazione | Cos'è |
|---|---|
webhook-id | L'id dell'evento (lo stesso id del corpo) |
webhook-timestamp | Quando è partito il tentativo, in secondi dal 1970 |
webhook-signature | v1, 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
2xxentro 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.renewedpuò arrivare prima delsubscription.createdche era in nuovo tentativo. UsaoccurredAtper ordinare e, quando ti serve lo stato attuale, chiedi all'API. - I reindirizzamenti non vengono seguiti: un
301o302conta 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 risposta | Cosa succede |
|---|---|
2xx | Consegnata |
410 Gone | Il webhook viene disattivato subito, senza altri tentativi, e proprietario e amministratori ricevono un'e-mail |
429 | Riprova dopo il Retry-After (in secondi o come data), se presente; non conta come errore |
| Qualsiasi altro stato, tempo scaduto o errore di rete | Errore; 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
| Limite | Valore |
|---|---|
| Webhook per community | 10 |
| Creare, inviare test, riattivare, reinviare e cambiare l'URL | 10 al minuto per community, sommati; oltre, 429 con Retry-After |
| Cronologia delle consegne e delle modifiche | 30 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}'
| Campo | Tipo | Nota |
|---|---|---|
url | testo (fino a 2048) | Stesso controllo della creazione (https, porta 443, indirizzo pubblico); conta nel limite di 10 al minuto per community |
description | testo (fino a 200) o null | |
events | lista (da 1 a 50) | Tipi o famiglie intere (member.*) |
fixedIps | booleano | Attiva 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'
urlazzerafailureCount. - Modificare non riattiva: un webhook
DISABLEDoDISABLED_FAILINGresta 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
updatednella cronologia, con il prima e il dopo di ogni campo inchanges(URL mascherati). Una modifica senza cambiamenti non genera righe. - Risposte:
200con il webhook,404,422(campo non valido, inerrors[].param) e429.
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.