Webhookok
A Posty szól a rendszerednek, ha egy bejegyzés kiment vagy elbukott. Események, a törzs alakja és az aláírás ellenőrzése.
Wofür das da ist
Ein Webhook ist die umgekehrte Richtung: Du fragst Posty nicht ab, wir benachrichtigen dein System, wenn etwas passiert. Ein Beitrag ist rausgegangen, eine Veröffentlichung ist fehlgeschlagen, ein Kanal wurde getrennt.
Die Funktion gibt es ab dem Pro-Tarif, im linken Menü unter Automatisierungen, auf dem Tab Webhooks. Dein Tarif bestimmt, wie viele Webhooks du haben kannst.
Webhook erstellen
- 1Webhook hinzufügenGib ihm einen Namen und die Adresse deines Endpunkts, der die Anfragen empfängt. Es muss eine HTTPS-Adresse sein.
- 2Wähle die EreignisseDu kannst ein paar auswählen oder die Liste leer lassen. Eine leere Liste bedeutet alles, auch Ereignisse, die später dazukommen.
- 3Wähle die KanäleDu kannst den Webhook auf bestimmte Kanäle einschränken, wenn dich nur die interessieren.
- 4Speichere ihn und schreib das Signatur-Secret aufDas Secret beginnt mit dem Präfix
whsec_. Im Webhook-Editor kannst du es jederzeit ansehen und neu generieren.
Wenn du kein Ereignis auswählst, bekommst du alle, auch die, die wir später hinzufügen. Wenn du sechs auswählst, bekommst du genau diese sechs, und ein siebtes, später hinzugefügtes Ereignis wird an diesen Webhook nicht zugestellt.
Die Ereignisse
| Ereignis | Wann |
|---|---|
post.published | Ein Beitrag ist auf einen Kanal rausgegangen. |
post.failed | Ein Beitrag ist im Status Fehler gelandet: Die Plattform hat ihn abgelehnt, das Token ist ungültig geworden, der Kanal war offline oder deaktiviert, oder das Limit des Tarifs hat es nicht zugelassen. |
channel.disconnected | Der Zugriff ist weggefallen, und der Kanal ist offline gegangen. Die Warteschlange dieses Kanals gibt nichts mehr raus, bis du ihn erneut verbindest. |
channel.refresh_needed | Die Token-Erneuerung ist auf halbem Weg stecken geblieben. Der Kanal ist nicht offline, aber die Verbindung musst du manuell zu Ende führen. |
channel.disabled | Der Kanal ist deaktiviert: Entweder hast du ihn ausgeschaltet, oder der Abrechnungsabgleich hat ihn nach einem Tarifwechsel deaktiviert. |
channel.enabled | Ein ausgesetzter Kanal wurde wieder eingeschaltet. |
Es gibt kein channel.connected-Ereignis: Das Verbinden ist ein mehrstufiger Ablauf, und die Zwischenzustände lohnen sich nicht zu melden.
Jede Zustellung trägt auch einen Header X-Posty-Event, aber verzweige auf das Feld event im Body. Der Header steckt nicht in der Signatur, das Feld schon.
Der Request-Body
Der Body ist immer ein JSON-Array, bei jedem Ereignis. Nimm nicht an, dass die Länge immer eins ist.
post.published
[
{
"id": "cm6tcts4f0005qcwit25cis26",
"content": "Ez az első bejegyzés Instagramra",
"publishDate": "2026-09-15T09:00:00.000Z",
"releaseURL": "https://instagram.com/p/...",
"state": "PUBLISHED",
"integration": {
"id": "cm6s4uyou0001i2r47pxix6z1",
"name": "Posty",
"providerIdentifier": "instagram",
"picture": "https://uploads.posty.hu/...jpeg",
"type": "social"
},
"event": "post.published"
}
]publishDate ist UTC, wie jeder Zeitstempel in Posty.
post.failed
Dasselbe Objekt, plus das Feld error. state ist ERROR, und releaseURL ist null, weil nichts veröffentlicht wurde. error ist der eigene Satz der Plattform, falls es einen gibt, sonst null.
channel-Ereignisse
[
{
"event": "channel.disconnected",
"reason": "token_revoked",
"detail": "— access was removed in the account's Meta settings",
"integration": {
"id": "cm6s4uyou0001i2r47pxix6z1",
"name": "Posty",
"providerIdentifier": "instagram",
"picture": "https://uploads.posty.hu/...jpeg",
"type": "social"
}
}
]reason ist eine geschlossene Menge: token_revoked, refresh_failed, manual, plan_limit. Verzweige darauf. detail ist Freitext auf Englisch oder null: Das liest ein Mensch, nicht dein Code.
Die Signatur prüfen
Jede Zustellung ist signiert, im Header X-Posty-Signature:
X-Posty-Signature: t=1770000000,v1=10bd17b41e83eee170010df01daeeaa0edf2f939afa6e97f41e1fa7bd27e6191t: Unix-Zeit in Sekunden, UTC, im Moment der Signatur.v1: hexadezimal in KleinbuchstabenHMAC-SHA256(Secret, "<t>.<roher Body>").- Der Schlüssel ist das eigene Signatur-Secret des Webhooks, mit dem Präfix
whsec_.
Das ist absichtlich dieselbe Form wie bei der Stripe-Signatur, damit vorhandene Stripe-Prüfbibliotheken damit funktionieren. Du musst nur den Header-Namen und das Secret anpassen.
JSON.stringify ist nicht kanonisch: Die Reihenfolge der Schlüssel, das Unicode-Escaping und die Formatierung der Zahlen unterscheiden sich je nach Implementierung. Ein empfangendes System, das den Body erst verarbeitet und dann wieder zum String macht, berechnet einen anderen Hashwert und lehnt eine einwandfreie Zustellung ab. Lies den Body vor der Verarbeitung roh aus.
Prüfung, Schritt für Schritt
- Lies den rohen Body und den Header X-Posty-Signature aus.
- Teile den Header am Komma, dann jeden Teil am ersten Gleichheitszeichen. So bekommst du t und v1.
- Lehne ab, wenn die Differenz zwischen t und der aktuellen Zeit größer als die Toleranz ist. Wir rechnen mit 300 Sekunden Toleranz. Das ist der Replay-Schutz.
- Berechne HMAC-SHA256(Secret, t + "." + roherBody) als hexadezimalen Wert in Kleinbuchstaben.
- Vergleiche das Ergebnis mit v1, mit einem zeitkonstanten Vergleich.
const crypto = require('crypto');
function verify(secret, rawBody, header, toleranceSeconds = 300) {
const parts = Object.fromEntries(
header.split(',').map((p) => {
const at = p.indexOf('=');
return [p.slice(0, at).trim(), p.slice(at + 1).trim()];
})
);
const t = Number(parts.t);
if (!Number.isFinite(t)) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
return (
expected.length === parts.v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
);
}Um eine mitgeschnittene Zustellung von Hand zu prüfen, dasselbe in der Shell:
printf '%s' "$T.$RAW_BODY" | openssl dgst -sha256 -hmac "$SECRET" -hexv1 ist absichtlich versioniert: Wenn sich das Schema jemals ändert, tragen die Zustellungen für eine Übergangszeit das alte und das neue Element. Lies das Element, dessen Namen du verstehst, und ignoriere den Rest. Geh nicht davon aus, dass der Header genau zwei Teile hat.
Was das empfangende System wissen muss
- Verzweige auf das Feld
eventim Body, bevor du irgendetwas anderes liest, und ignoriere unbekannte Ereignisse: Es werden mehr davon kommen. - Nimm das Feld
iddes Beitrags als Schlüssel, oder das Paar ausintegration.idund dem Ereignis, und behandle eine wiederholte Zustellung idempotent. - Erwarte eine Zustellung pro Kanal, nicht eine pro Gruppe, und rechne nicht mit einer bestimmten Reihenfolge.
- Bei falscher Signatur lehne die Anfrage ab und schreib sie ins Log. Nur so wird ein gefälschtes POST sichtbar.
Wenn du nicht Ereignisse empfangen, sondern abfragen willst, gibt der Endpunkt Benachrichtigungen dieselben Ereignisse als Liste zurück.