Webhooks
Ein Webhook dreht die Richtung um: statt zu fragen, ob sich etwas getan hat, bekommst du es gesagt. Wir senden POST an deine Adresse, signiert, mit Wiederholungen — und mit einer Grenze, die hier wichtiger ist als der Rest.
Die Grenze zuerst
Ein Endpunkt hängt am Legacy-Zugang deines Projekts, nicht an einem OAuth-Grant. Er trägt damit die Einwilligung keiner Organisation und keines Teams. Daraus folgt die Regel, an der die ganze Ereignisliste hängt:
Ohne OAuth keine Produktdaten. Auch nicht über den Umweg Webhook.
Deshalb gibt es zwei Klassen von Ereignissen:
| Klasse | Was in der Nutzlast steht | Beispiel |
|---|---|---|
| Voll | Der Vorgang vollständig — es sind deine eigenen Daten. | unit.state.changed, key.revoked, quota.exceeded |
| Signal | Ressource, Kennung, Aktion. Kein Name, kein Betrag, kein Feld. | mc.members.changed, connection.unlinked |
Ein Signal sagt dir nie mehr, als du durch Pollen ohnehin erführest — es erspart dir nur das Pollen. Was sich geändert hat, holst du danach mit deinem OAuth-Token über die API, mit demselben Scope wie sonst auch.
Was es zu abonnieren gibt
Die Konsole zeigt unter Webhooks genau die Ereignisse, die dein Projekt haben kann: freigeschaltete API, freigeschaltetes Ereignis. Die Gruppen:
| Gruppe | Klasse | Worum es geht |
|---|---|---|
| API Keys | voll | Erstellt, rotiert, widerrufen, abgelaufen — und öffentlich gefunden. |
| OAuth Clients | voll | Angelegt, Secret rotiert, abgeschaltet. |
| Sandboxes | voll | Angelegt, zurückgesetzt, gelöscht. |
| Projekt & Kontingent | voll | API an oder aus, Kontingent bei 80 %, Kontingent erschöpft, Zugang gewechselt. |
| Team | voll | Einladung, Rollenwechsel, Entfernung. |
| Plattform | voll | Störungen der Statusseite und Abkündigungen von API-Versionen. |
| Zustellung | voll | Ein Endpunkt wurde nach zu vielen Fehlschlägen stillgelegt. |
| Verbindung zum Produkt | Signal | Konto verknüpft, Verknüpfung gelöst, Freigabe geändert, Verlängerung gescheitert. |
| Änderungssignale | Signal | Je Produkt-API eines: mc.members.changed, sb.servers.changed und so fort. |
connection.unlinked ist das Ereignis, für das sich der ganze Aufwand lohnt: bis heute merkt eine Integration den Entzug einer Freigabe erst am nächsten 401 — mit Webhook in dem Moment, in dem er passiert.
Die Hülle
Jede Nutzlast sieht gleich aus. Was der Vorgang mitbringt, steht unter data.
{
"id": "evt_9f2c81ab7d3e4405",
"type": "unit.state.changed",
"created_at": "2026-08-18T09:14:22.481Z",
"environment": "production",
"data": { "unit": "u_4471", "state": "wartung", "reason": "Drehmoment" }
}
Ein Signal trägt dieselbe Hülle und ein karges data:
{
"id": "evt_2b90ce41d78a6612",
"type": "mc.members.changed",
"created_at": "2026-08-18T09:15:03.117Z",
"environment": "production",
"data": {
"api": "methacore-members",
"owner": "6654994a1f2b3c4d5e6f7a8b",
"resource": "mc.members.profile",
"id": "66bcfadc0a1b2c3d4e5f6071",
"action": "update"
}
}
Kein Name, keine Anschrift, kein Geburtsdatum. Für die Daten: GET /v1/members/66bcfadc… mit deinem Token.
Signatur prüfen
Jede Anfrage trägt Kopfzeilen:
| Header | Inhalt |
|---|---|
X-Artim-Event | Der Ereignisname, z. B. unit.state.changed |
X-Artim-Delivery | Die Kennung der Zustellung — dieselbe wie id im Körper |
X-Artim-Webhook-Id | Welcher deiner Endpunkte gemeint ist |
X-Artim-Environment | production oder sandbox |
X-Artim-Signature | t=<sekunden>,v1=<hmac> |
Signiert wird "<sekunden>." + roher Körper mit HMAC-SHA256 und deinem Signing Secret. Der Zeitstempel steht mit im signierten Text: sonst ließe sich ein mitgeschnittener Aufruf später erneut abspielen. Wir verwerfen Signaturen, die älter als fünf Minuten sind — tu dasselbe.
import crypto from "node:crypto";
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t));
if (!Number.isFinite(age) || age > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.` + rawBody)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
Wichtig ist roh: der Körper, wie er ankam, Byte für Byte. Wer erst JSON parst und dann wieder serialisiert, prüft die Signatur einer anderen Zeichenkette.
Das Secret erzeugen wir beim Anlegen und zeigen es genau einmal. Es beginnt mit whsec_live_ oder whsec_sbx_ — daran siehst du, aus welcher Umgebung eine Nutzlast kommt, noch bevor du sie liest.
Content type
Zwei Verpackungen, derselbe Inhalt:
application/json— der Körper ist das JSON.application/x-www-form-urlencoded— der Körper istpayload=<json>, für Empfänger, die nur Formulare kennen.
Signiert wird in beiden Fällen der rohe Körper, nicht das JSON darin.
Wiederholungen
Ein Versuch hat zehn Sekunden. Nimm die Nutzlast entgegen, antworte mit 2xx und arbeite danach — wer im Handler die halbe Buchhaltung durchrechnet, läuft in die Zeitüberschreitung.
Antwortet dein Endpunkt nicht mit 2xx, versuchen wir es erneut: nach einer Minute, nach fünf, nach dreißig, nach zwei Stunden, nach sechs. Danach steht die Zustellung als failed im Protokoll und lässt sich in der Konsole von Hand erneut senden.
Zwanzig fehlgeschlagene Zustellungen in Folge legen den Endpunkt still. Du erfährst es über webhook.endpoint.disabled — zugestellt an deine anderen Endpunkte, denn der stillgelegte ist ja gerade der, der nicht antwortet.
Dieselbe Kennung kann zweimal kommen: eine Wiederholung nach einer Zeitüberschreitung, obwohl dein Dienst schon verbucht hatte. Merke dir id und verwirf, was du kennst.
SSL-Prüfung
Standardmäßig prüfen wir das Zertifikat deines Endpunkts. Abschalten lässt sich das für eigene Testempfänger — mit der Folge, dass jeder in der Leitung die Nutzlast mitlesen und verändern kann. Die Signatur schützt dann noch die Echtheit, nicht die Vertraulichkeit.
Die Adresse muss https sein und aus dem Internet erreichbar: Endpunkte, die auf interne Adressen zeigen, lehnen wir beim Anlegen ab.
Ausprobieren
Ein frisch angelegter Endpunkt bekommt sofort einen ping — wer die Signaturprüfung falsch gebaut hat, merkt es in derselben Minute. Später jederzeit über Test senden im Zeilenmenü. ping ist nicht abonnierbar; er kommt immer.
Im Zustellprotokoll steht je Versuch der Status, die Dauer und der Fehlertext deines Servers — der bleibt gekürzt auf zwei Kilobyte, aber es reicht, um den eigenen Stacktrace wiederzuerkennen.