Webhooks
Für Integratoren. Wie Memberfy dein System benachrichtigt, wenn in der Community etwas passiert, das Format des Umschlags, die Standard-Webhooks-Signatur und wie du sie in Node prüfst, Deduplizierung, Reihenfolge, Wiederholungen, automatisches Ausschalten und die Limits.
Ein Webhook ist Memberfy, das dein System aufruft: Wenn jemand beitritt, kauft, abonniert, sich für eine Veranstaltung anmeldet oder einen Kurs abschließt, sendet Memberfy sofort einen signierten POST an die URL, die du hinterlegt hast. Es ist der umgekehrte Weg der API, bei der dein System fragt.
Den Webhook legt der Eigentümer oder ein Administrator der Community an, unter Einstellungen › Integrationen und APIs, Tab Webhooks. Siehe Integrationen & APIs › Webhooks. Diese Anleitung ist für alle, die den Code schreiben, der ihn empfängt.
Was ankommt
Ein POST pro Ereignis und pro Webhook, mit Content-Type: application/json; charset=utf-8 und User-Agent: Memberfy-Webhooks/1. Der Inhalt ist immer derselbe Umschlag:
{
"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": "Pro-Plan", "type": "SUBSCRIPTION" },
"amount": { "amount": 9990, "currency": "BRL" }
}
}
| Feld | Was es ist |
|---|---|
id | Die Kennung des Ereignisses. Sie ist dieselbe wie im Header webhook-id, bei allen Versuchen und beim erneuten Senden |
type | Das Ereignis (member.joined, purchase.completed…) oder ping, der Test |
version | Die Version des data-Formats dieses Ereignisses. Heute überall 1 |
occurredAt | Wann das Ereignis bei Memberfy passiert ist (ISO 8601, UTC), nicht wann die Zustellung losging |
community | Die Community: id und slug |
data | Was passiert ist. Unterscheidet sich je Ereignis; siehe Webhook-Ereignisse |
Beträge kommen in Cent, etwa { "amount": 9990, "currency": "BRL" }, immer das, was die Person bezahlt hat. Die beteiligte Person steht in data.member, mit id, name, email, phone (im Format E.164, etwa +5541999998888, oder null) und phoneCountry (das Land der Telefonnummer nach ISO 3166-1 Alpha-2, etwa BR, oder null).
Die Ereignisse
Es sind 97 Ereignisse in 27 Familien, dazu der ping:
| Familie | Ereignisse |
|---|---|
member | member.joined (auch für per Tabelle importierte Personen, mit 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 (mit den Antworten zur Anmeldung 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 (ohne den Text der Nachricht) |
badge | badge.awarded, badge.revoked |
| (Test) | ping, gesendet über den Button Test senden und beim Wiedereinschalten, mit data: { "message": "…" } |
Ein Webhook abonniert Ereignisse einzeln oder die ganze Familie (member.*); wer die Familie ankreuzt, bekommt auch Ereignisse, die später dazukommen. Die Felder jedes Ereignisses stehen unter Webhook-Ereignisse.
Eine noch nicht angenommene Einladung erzeugt kein member.joined.
Level und Punkte
member.level_changed geht raus, wenn sich das Level der Person ändert:
{
"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(aufgestiegen, sofort) oderdown(abgestiegen).cause:points(die Punkte haben das Level geändert) oderactivity(die Anforderungen der Woche: erfüllt, beim Aufstieg, oder in der Vorwoche nicht erfüllt, beim Abstieg am Montag um 00:00 in der Zeitzone der Community).fromundto: das Level vorher und nachher, mitid,level(die Nummer, 1 ist das erste) undname. Dieidistnull, solange die Community die Standard-Level nutzt und sie nie geöffnet hat.- Mehrere Level auf einmal erzeugen ein Ereignis, mit dem Endlevel in
to.
member.points_changed geht raus, wenn sich die Punktesumme der Person in der Community ändert: from, to, delta und source (action, eine Aktion der Person; reversal, zurückgenommene Punkte, weil die Aktion rückgängig gemacht wurde; team, eine Anpassung durch das Team; workflow, eine Anpassung durch einen Workflow).
Keines der beiden geht bei einer Neuberechnung der Punkte, einem Import, einer Änderung der Level durch die Community oder bei ausgeschalteter Gamification raus.
Was nie mitgeschickt wird
Kein Ereignis enthält Gebühren, Nettobeträge, Daten des Zahlungsanbieters oder der Karte, Steuernummern aus dem Konto, Adresse, Geburtsdatum, benutzerdefinierte Felder, Passwörter, Tokens oder Secrets, IP oder den Text von Beiträgen, Kommentaren und Tickets. Die Antworten auf ein Formular (form.submitted) und auf die Anmeldefragen einer Veranstaltung (event.registered) gehen so mit, wie die Person sie ausgefüllt hat, einschließlich des Felds Ausweis. Name, E-Mail und Telefonnummer der Person (wenn sie eine hinterlegt hat) sind immer dabei: Deshalb weist die Oberfläche beim Erstellen des Webhooks darauf hin, dass personenbezogene Daten an das Ziel gehen.
Die Signatur prüfen
Jede Zustellung ist nach Standard Webhooks signiert, mit dem Secret des Webhooks (beginnt mit whsec_ und wird nur einmal angezeigt, beim Erstellen oder Rotieren). Drei Header:
| Header | Was es ist |
|---|---|
webhook-id | Die ID des Ereignisses (dieselbe id wie im Inhalt) |
webhook-timestamp | Wann der Versuch losging, in Sekunden seit 1970 |
webhook-signature | v1, gefolgt vom HMAC-SHA256 in Base64 von {webhook-id}.{webhook-timestamp}.{Inhalt} |
Prüfe immer, bevor du dem Inhalt vertraust, mit dem Inhalt genau so, wie er angekommen ist (ohne ihn vorher zu parsen), und lehne einen webhook-timestamp ab, der mehr als 5 Minuten von deiner Uhr abweicht. Die Bibliothek standardwebhooks erledigt beides. In Node mit Express:
import express from 'express';
import { Webhook } from 'standardwebhooks';
const app = express();
const webhook = new Webhook(process.env.MEMBERFY_WEBHOOK_SECRET); // whsec_…
// Der rohe Inhalt: Die Signatur gilt für die angekommenen Bytes.
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); // deduplizieren
res.sendStatus(200); // sofort antworten, später verarbeiten
await queue.add(event); // z. B. deine Job-Warteschlange
});
app.listen(3000);
verify prüft Signatur und Zeit und liefert den bereits gelesenen Umschlag. Bibliotheken für denselben Standard gibt es für Python, Go, Ruby, PHP, Java und weitere Sprachen.
Beim Rotieren des Secrets gilt das neue sofort und das alte nicht mehr: Aktualisiere deine Seite direkt danach.
Antworten und verarbeiten
- Antworte innerhalb von 10 Sekunden mit einem beliebigen
2xx. Danach zählt der Versuch als Fehler. - Verarbeite nach der Antwort (eine Warteschlange, ein Job): Eine langsame Integration darf die Antwort nicht aufhalten.
- Dedupliziere über die
webhook-id. Dasselbe Ereignis kann mehrmals ankommen (eine im Netz verlorene Antwort, ein erneutes Senden über die Oberfläche): Speichere die schon verarbeiteten IDs und ignoriere Wiederholungen. - Die Reihenfolge ist nicht garantiert. Ein
subscription.renewedkann vor demsubscription.createdankommen, das gerade wiederholt wurde. Sortiere nachoccurredAtund frag die API, wenn du den aktuellen Stand brauchst. - Weiterleitungen werden nicht verfolgt: Ein
301oder302zählt als Fehler. Hinterlege die endgültige URL.
Wiederholungen
Ohne 2xx versucht Memberfy es erneut. Es sind bis zu 8 Versuche pro Zustellung: der erste sofort, die weiteren nach 1 Min., 5 Min., 30 Min., 2 Std., 6 Std., 12 Std. und 24 Std., insgesamt etwa 45 Stunden. Nach allen 8 bleibt die Zustellung im Verlauf als Fehlgeschlagen, und sie lässt sich über die Oberfläche erneut senden (ein neuer Zyklus, mit derselben webhook-id).
| Die Antwort | Was passiert |
|---|---|
2xx | Zugestellt |
410 Gone | Der Webhook wird sofort ausgeschaltet, ohne weitere Versuche, und Eigentümer und Administratoren erhalten eine E-Mail |
429 | Neuer Versuch nach dem Retry-After (in Sekunden oder als Datum), falls vorhanden; zählt nicht als Fehler |
| Jeder andere Status, Zeitüberschreitung oder Netzwerkfehler | Fehler; folgt dem Zeitplan oben |
Automatisches Ausschalten
Bleibt das Ziel 72 Stunden ohne erfolgreiche Zustellung (mit mindestens 5 Fehlern in dieser Zeit), wird der Webhook ausgeschaltet (Wegen Fehlern ausgeschaltet), ausstehende Versuche werden abgebrochen, und Eigentümer und Administratoren erhalten eine E-Mail. Ereignisse, die währenddessen passieren, werden später nicht gesendet.
Zum Zurückholen nutze Wieder einschalten in der Oberfläche: Memberfy sendet zuerst einen ping, und der Webhook kommt nur zurück, wenn das Ziel mit 2xx antwortet.
Adresse und Netzwerk
- Nur
https, auf Port 443, an einer öffentlichen Adresse. Interne Adressen, private Netze und Loopback werden abgelehnt. - Keine Weiterleitung und ein Limit von 10 Sekunden pro Versuch. Von der Antwort speichert Memberfy nur den Anfang (1 KB) für den Verlauf.
- Akzeptiert deine Firewall nur bekannte Adressen, kreuze beim Webhook Von festen IPs senden an und gib die festen IPs von Memberfy frei.
Limits
| Limit | Wert |
|---|---|
| Webhooks pro Community | 10 |
| Erstellen, Test senden, wieder einschalten, erneut senden und URL ändern | 10 pro Minute pro Community, zusammen; darüber 429 mit Retry-After |
| Verlauf der Zustellungen und Änderungen | 30 Tage |
Jede erfolgreiche Zustellung eines Ereignisses zählt bei Zugestellte Webhooks im Block Nutzung von Integrationen & APIs. Der ping zählt nicht.
Per API verwalten
Die Oberfläche nutzt die Webhook-API unter https://developers.memberfy.net (/api/webhooks: auflisten, erstellen, URL, Beschreibung, Ereignisse und feste IPs bearbeiten, Test senden, rotieren, ausschalten, wieder einschalten, löschen, Zustellungen, erneutes Senden und Verlauf). Sie akzeptiert nur das Anmelde-Token (Authorization: Bearer) mit der X-CommunityId, von Eigentümer oder Administrator; ein API-Schlüssel kommt nicht hinein. Die Spezifikation liegt unter developers.memberfy.net/docs.json. Der Ereigniskatalog ist öffentlich: GET /api/integrations/events.
Einen Webhook bearbeiten
PATCH /api/webhooks/{id} nur mit dem, was sich ändert; ein fehlendes Feld bleibt, wie es ist.
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}'
| Feld | Typ | Hinweis |
|---|---|---|
url | Text (bis 2048) | Dieselbe Prüfung wie beim Erstellen (https, Port 443, öffentliche Adresse); zählt zum Limit von 10 pro Minute und Community |
description | Text (bis 200) oder null | |
events | Liste (1 bis 50) | Typen oder ganze Familien (member.*) |
fixedIps | Boolean | Schaltet Von festen IPs senden ein oder aus |
- Die neue Konfiguration gilt für Ereignisse, die danach passieren. Ausstehende Zustellungen, Wiederholungen und späteres erneutes Senden laufen so, wie sie erstellt wurden: dieselbe URL, dieselbe Nutzung fester IPs und derselbe Payload. Die Signatur nutzt immer das aktuelle Secret.
- Eine neue
urlsetztfailureCountzurück. - Bearbeiten schaltet nicht wieder ein: Ein Webhook
DISABLEDoderDISABLED_FAILINGbleibt aus. Wieder einschalten istPOST /api/webhooks/{id}/enable, das den Test sendet und nur bei einer 2xx-Antwort einschaltet. - Jede Bearbeitung, die etwas ändert, wird zu einer Zeile
updatedim Verlauf, mit Vorher und Nachher jedes Felds inchanges(URLs maskiert). Eine Bearbeitung ohne Änderung erzeugt keine Zeile. - Antworten:
200mit dem Webhook,404,422(ungültiges Feld, inerrors[].param) und429.
Fertige Integrationen
Die fertigen Integrationen (Discord, Slack, Telegram, RD Station, HubSpot) nutzen dieselbe API unter /api/apps: GET /api/apps (Katalog, mit der Installation jeder App), PUT /api/apps/{slug}/installation (schaltet ein oder speichert Konfiguration und Ereignisse), POST …/installation/test, DELETE …/installation (schaltet aus; mit ?remove=true wird sie entfernt und Konfiguration und Zustellungen gelöscht), POST /api/apps/telegram/chats (Meine Gruppe finden) und die Zustellungen unter …/installation/deliveries, mit erneutem Senden. Dieselben Zugriffsregeln: Login-Token von Eigentümer oder Administrator, nie ein API-Schlüssel. Geheime Felder kommen maskiert zurück; bei einem PUT behält ein fehlendes geheimes Feld den gespeicherten Wert.