Webhooks
Ein Webhook ist eine Adresse auf deinem Server, die Sway mit einem POST aufruft, sobald etwas passiert: eine Bestellung wird bezahlt, eine Veranstaltung geht live, jemand beantwortet ein Formular. Deine Site muss die API nicht mehr alle paar Minuten fragen, ob sich etwas geändert hat; sie erfährt es innerhalb von Sekunden.
Webhooks richtest du in der Web-App ein, nicht über die API: Crew-Admin → API → Webhooks. Sie gehören zur API, auf den Plänen Studio und Roster, und wer sie einrichtet, braucht die Berechtigung API verwalten auf der Crew.
Einen Webhook hinzufügen
- Öffne Crew-Admin → API, dann den Tab Webhooks, und klick auf Webhook hinzufügen.
- Gib die Endpoint-URL ein: eine öffentliche
https-Adresse auf deinem Server, zum Beispielhttps://www.example.com/webhooks/sway. - Hak die Ereignisse an, die er empfangen soll (siehe die Liste unten).
- Lass Personenbezogene Daten mitsenden aus, außer der Endpunkt braucht Namen und E-Mail-Adressen (siehe Personenbezogene Daten).
- Klick auf Webhook hinzufügen. Das Signatur-Secret erscheint einmal, beginnend mit
whsec_. Kopier es in die Konfiguration deines Servers: Sway zeigt es nie wieder.
Schick dir sofort einen Aufruf mit Test-Ereignis senden: Eine webhook.test-Zustellung trifft innerhalb von Sekunden ein und erscheint unter Zustellungen.
Eine Crew kann bis zu 10 Webhooks haben.
Adressen, die Sway nicht aufruft
Die Adresse muss https nutzen, einen Host benennen (keine IP-Adresse), und sich in eine öffentliche Adresse auflösen, wenn Sway den Aufruf sendet. Sway lehnt localhost, private und link-lokale Netze, Adressen mit Benutzername oder Passwort darin, und Ports unter 1024 außer 443 ab. Weiterleitungen werden nicht verfolgt: Zeig mit dem Webhook auf die endgültige Adresse.
Um Webhooks auf einer Entwicklungsmaschine zu empfangen, mach sie über einen Tunnel erreichbar, der dir eine öffentliche https-Adresse gibt.
Die Ereignisse
| Typ | Gesendet wenn | Nennt eine Person |
|---|---|---|
order.paid | Eine Bestellung für eine deiner Veranstaltungen wird bezahlt, auf Sway oder über deine Site | Ja: die E-Mail des Käufers |
order.refunded | Eine Bestellung wird ganz oder teilweise erstattet (ein Aufruf pro Erstattung) | Ja: die E-Mail des Käufers |
event.published | Eine veröffentlichte Veranstaltung wird für deine Crew sichtbar: Sie geht live, oder eine deiner Seiten wird ihr hinzugefügt | Nein |
event.updated | Eine Veranstaltung, die du siehst, ändert sich: Datum, Zeiten, Titel, Beschreibung, Bild, Veranstaltungsort, Lineup, Status, oder sie wird offline genommen | Nein |
booking_request.created | Eine Booking-Anfrage kommt über POST /v1/booking-requests herein | Ja: der Kontakt |
ambassador.sale_attributed | Ein bezahlter Verkauf wird einem deiner Ambassadors gutgeschrieben | Ja: der Name des Ambassadors |
form.submitted | Jemand füllt eines deiner Formulare aus | Ja: die Antworten und der Kontakt |
payout.paid | Eine Auszahlung erreicht das Bankkonto einer deiner Seiten | Nein |
Wer was bekommt, folgt denselben Regeln wie die Lesezugriffe der API:
- Bestellungen und Auszahlungen gehen an die Crews, die die Seite verwalten, an die das Geld geht. Eine Crew, die einen Künstler nur ins Lineup bucht, erfährt nichts von Ticketverkäufen.
- Veranstaltungen gehen an jede Crew, deren Seiten an der Veranstaltung beteiligt sind: ihre Veranstalter, ihren Veranstaltungsort, ihre bestätigten Künstler.
- Ambassador-Verkäufe gehen an die Crews, die die Seite des Programms verwalten.
- Formulare gehen an die Crew, der das Formular gehört, oder die die Seite verwaltet, zu der es gehört.
Sway prüft direkt vor dem Senden noch einmal. Eine Crew, die die Seite zwischenzeitlich verloren hat, bekommt nichts.
event.published und event.updated
event.published ist der Aufruf, der deiner Site sagt, eine Veranstaltung hinzuzufügen. Er wird gesendet, wenn eine Veranstaltung veröffentlicht wird, und auch, einmal pro Webhook, wenn eine deiner Seiten einer schon veröffentlichten Veranstaltung hinzugefügt wird (die Veranstaltung ist gerade zu deiner Crew gestoßen). Der erste Aufruf einer neuen Veranstaltung wartet 30 Sekunden, damit der Veranstaltungsort, das Cover und der Rest der Erstellung im selben Payload ankommen.
event.updated wartet 60 Sekunden, und Änderungen, die währenddessen gemacht werden, reisen mit: Zehn schnelle Bearbeitungen ergeben einen Aufruf. Er wird auch gesendet, wenn eine Veranstaltung offline genommen wird; ihr Payload sagt dann nur "published": false und sonst nichts, damit deine Site die Veranstaltung entfernen kann.
Liest dein Server nach einem dieser Aufrufe die API neu (die Veranstaltung, eine Liste, in der sie steht), schick diese Anfragen mit Cache-Control: no-cache: Sway liest sie dann frisch, statt mit einer Kopie von vor der Änderung zu antworten. Siehe Caching.
Wie ein Aufruf aussieht
POST /webhooks/sway HTTP/1.1
Host: www.example.com
Content-Type: application/json
User-Agent: Sway-Webhooks/1.0 (+https://www.sway.events/docs/api)
webhook-id: msg_3b1f7a52-2c4e-4d8f-9a61-7e0c5d2b9f14
webhook-timestamp: 1759525447
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{"type":"order.paid","timestamp":"2026-10-03T21:04:07.000Z","data":{"order":{...}}}
Der Body ist der Standard Webhooks-Umschlag:
| Feld | Bedeutung |
|---|---|
type | Der Ereignistyp, aus der Tabelle oben |
timestamp | Wann es passiert ist, ISO 8601 in UTC |
data | Das Objekt, gelesen von Sway im Moment, in dem der Aufruf gesendet wird, also immer der aktuelle Stand |
webhook-id identifiziert das Ereignis. Es bleibt bei jeder Wiederholung und bei einem manuellen Erneut senden gleich: Nutz es, um einen Aufruf zu ignorieren, den du schon verarbeitet hast.
Payloads
Beträge sind Ganzzahlen in der kleinsten Einheit der Währung (Cent), Währungen sind ISO-4217-Codes.
order.paid und order.refunded
{
"type": "order.paid",
"timestamp": "2026-10-03T21:04:07.000Z",
"data": {
"order": {
"id": "0f5c2a8e-7b1d-4c3a-9e6f-2d8b4a1c7e53",
"event_id": 4521,
"status": "paid",
"currency": "EUR",
"amount_total": 4600,
"amount_refunded": 0,
"created_at": "2026-10-03T21:03:51.219+00:00",
"lines": [
{ "tier_id": "8a3e1f20-5c6d-4b7a-8e9f-1a2b3c4d5e6f", "name": "Early bird", "quantity": 2, "unit_amount": 2300 }
],
"ambassador_code": "LUCIA10",
"via_your_site": true,
"buyer": { "email": "[email protected]" }
}
}
}
via_your_site ist true, wenn die Bestellung über einen Checkout lief, den deine Site mit POST /v1/checkout-sessions geöffnet hat. buyer ist null bei einem Webhook ohne personenbezogene Daten. Bei order.refunded ist amount_refunded die bisher insgesamt erstattete Summe.
event.published und event.updated
{
"type": "event.published",
"timestamp": "2026-10-01T09:30:00.000Z",
"data": {
"event": {
"id": 4521,
"title": "Insomnia Night: Autumn Opening",
"description": "Ten hours, two rooms.",
"starts_at": "2026-10-03T20:00:00+00:00",
"ends_at": "2026-10-04T06:00:00+00:00",
"timezone": "Europe/Brussels",
"image_url": "https://cdn.sway.events/events/4521/cover.jpg",
"type": "Party",
"page_url": "https://www.sway.events/event/4521",
"venue": { "id": 77, "name": "Fuse" },
"published": true
}
}
}
Die Veranstaltung ist die Kurzfassung, die die API in Listen liefert; hol GET /v1/events/{id} für das Lineup und den Rest. Eine offline genommene Veranstaltung:
{
"type": "event.updated",
"timestamp": "2026-10-02T14:12:00.000Z",
"data": { "event": { "id": 4521, "published": false } }
}
booking_request.created
{
"type": "booking_request.created",
"timestamp": "2026-10-02T11:45:10.000Z",
"data": {
"booking_request": {
"id": "5d2e9c41-3f7a-4b8e-a1c6-0e9f8d7c6b5a",
"artist_ids": [501],
"artist_names": ["Lucia Vega"],
"event_name": "Warehouse Series #4",
"event_date": "2026-12-12",
"venue": "Hangar 7",
"city": "Ghent",
"fee_offer": "2500 EUR",
"capacity": "800",
"ticket_price": "25 EUR",
"organization": "Nachtwerk vzw",
"status": "new",
"created_at": "2026-10-02T11:45:09.771+00:00",
"contact": { "name": "Jan Peeters", "email": "[email protected]", "phone": "+32470000000" }
}
}
}
ambassador.sale_attributed
{
"type": "ambassador.sale_attributed",
"timestamp": "2026-10-03T21:04:09.000Z",
"data": {
"sale": {
"order_id": "0f5c2a8e-7b1d-4c3a-9e6f-2d8b4a1c7e53",
"event_id": 4521,
"code": "LUCIA10",
"member_id": "c3a1e5b7-9d2f-4a6c-8e0b-1f3d5a7c9e2b",
"tickets": 2,
"amount_total": 4600,
"ambassador": { "first_name": "Lucia", "last_name": "Vega" }
}
}
}
form.submitted
{
"type": "form.submitted",
"timestamp": "2026-10-02T18:20:31.000Z",
"data": {
"response": {
"id": "e7b3c9d1-5a2f-4e8c-b6a0-9d1f3e5c7a2b",
"form": { "id": "1a9c7e5b-3d2f-4c8a-9e6b-0f2d4a6c8e1b", "slug": "guest-list", "title": "Guest list" },
"completed_at": "2026-10-02T18:20:30.412+00:00",
"locale": "en",
"answers": { "q_1": "Two guests", "q_2": ["Friday"] },
"contact": { "email": "[email protected]", "first_name": "Sam", "last_name": "Lee", "phone": null, "marketing_opt_in": true }
}
}
}
payout.paid
{
"type": "payout.paid",
"timestamp": "2026-10-06T08:00:12.000Z",
"data": {
"payout": {
"id": "9f1e3d5c-7b2a-4c6e-8a0d-2b4f6e8a1c3d",
"payee": { "type": "promoter", "id": 697 },
"amount": 120000,
"currency": "EUR",
"paid_at": "2026-10-06T07:59:58.103+00:00"
}
}
}
webhook.test, gesendet von Test-Ereignis senden:
{
"type": "webhook.test",
"timestamp": "2026-10-01T10:00:00.000Z",
"data": { "message": "A test delivery from Sway. Nothing happened.", "endpoint_id": "2c7e9a1b-4d3f-4e8a-b5c6-7d9e1f3a5b7c" }
}
Personenbezogene Daten
Standardmäßig aus, sagt ein Aufruf, was passiert ist, ohne zu sagen, wer: buyer, contact, answers und ambassador sind null. Schalt Personenbezogene Daten mitsenden ein, und sie tragen die Namen, E-Mail-Adressen und Antworten.
Nur der Crew-Eigentümer kann es einschalten, weil es personenbezogene Daten aus Sway auf einen Server schickt, den Sway nicht kontrolliert. Ein Mitglied mit der Berechtigung API verwalten kann es ausschalten. Wer den empfangenden Server betreibt, wird für diese Daten verantwortlich: Speicher nur, was du brauchst, und lösch es, sobald du es nicht mehr brauchst.
Die Signatur prüfen
Jeder Aufruf ist mit dem Secret des Webhooks signiert, auf die Art von Standard Webhooks, damit dein Server alles ablehnen kann, was nicht von Sway kommt:
- Nimm den rohen Body, genau wie empfangen. Das JSON zu parsen und neu zu serialisieren ändert die Bytes und bricht die Signatur.
- Bau den String
{webhook-id}.{webhook-timestamp}.{body}. - Berechne einen HMAC-SHA256 davon, mit dem Teil des Secrets nach
whsec_als Schlüssel, base64-dekodiert. - Vergleich ihn, base64-kodiert, mit jeder Signatur in
webhook-signature. Der Header ist eine leerzeichengetrennte Liste vonv1,{signature}; eine Übereinstimmung reicht. Nutz einen zeitkonstanten Vergleich. - Lehn einen
webhook-timestampab, der mehr als 5 Minuten von deiner Uhr abweicht, damit ein abgefangener Aufruf nicht später wiederholt werden kann.
Sway sendet jeden Aufruf mit einem Content-Length-Header. Die meisten Aufrufe wiegen wenige Kilobyte; eine Formularantwort mit langen Texten kann 1 MB überschreiten. Lehn vor dem Lesen des Bodys jeden Aufruf ab, der diesen Header nicht hat oder mehr angibt, als die Ereignisse, die du empfängst, wiegen können: Die Signatur lässt sich erst prüfen, wenn der Body gelesen ist, und so kann ein unsignierter Aufrufer den Speicher deines Servers nicht füllen.
Standard Webhooks veröffentlicht Verifier-Bibliotheken für viele Sprachen, darunter JavaScript, Python, PHP, Go und Ruby (Liste). In Node.js:
npm install standardwebhooks
import express from 'express'
import { Webhook } from 'standardwebhooks'
const wh = new Webhook(process.env.SWAY_WEBHOOK_SECRET) // whsec_...
const app = express()
// express.raw behält den Body als Bytes: Die Signatur bezieht sich auf diese Bytes.
app.post('/webhooks/sway', express.raw({ type: 'application/json' }), (req, res) => {
let event
try {
event = wh.verify(req.body, req.headers)
}
catch {
return res.status(400).end()
}
res.status(204).end() // erst antworten, dann arbeiten
queueSwayEvent(req.headers['webhook-id'], event)
})
Dieselbe Prüfung ohne Bibliothek, in Node.js:
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verifySway(rawBody, headers, secret) {
const id = headers['webhook-id']
const timestamp = Number(headers['webhook-timestamp'])
const signatures = headers['webhook-signature'] ?? ''
if (!id || !Number.isInteger(timestamp)) return false
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false
const key = Buffer.from(secret.slice('whsec_'.length), 'base64')
const expected = createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest()
return signatures.split(' ').some((part) => {
const [version, signature] = part.split(',')
if (version !== 'v1' || !signature) return false
const given = Buffer.from(signature, 'base64')
return given.length === expected.length && timingSafeEqual(given, expected)
})
}
Und in PHP, für eine WordPress- oder einfache PHP-Site:
<?php
$secret = getenv('SWAY_WEBHOOK_SECRET'); // whsec_...
$key = base64_decode(substr($secret, strlen('whsec_')));
$id = $_SERVER['HTTP_WEBHOOK_ID'] ?? '';
$timestamp = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
$signatures = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');
if ($id === '' || !ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
http_response_code(400);
exit;
}
$expected = base64_encode(hash_hmac('sha256', "$id.$timestamp.$body", $key, true));
$valid = false;
foreach (explode(' ', $signatures) as $part) {
[$version, $signature] = array_pad(explode(',', $part, 2), 2, '');
if ($version === 'v1' && hash_equals($expected, $signature)) {
$valid = true;
break;
}
}
if (!$valid) {
http_response_code(400);
exit;
}
http_response_code(204);
$event = json_decode($body, true);
// $event['type'], $event['data']: nach dem Antworten verarbeiten.
Schnell antworten, danach verarbeiten
Sway wartet 10 Sekunden auf eine Antwort und liest nur ihren Statuscode. Jedes 2xx zählt als zugestellt; alles andere, oder gar keine Antwort, ist ein Fehlschlag. Antworte, sobald die Signatur stimmt, und erledig die eigentliche Arbeit (einen Cache aktualisieren, in ein CRM schreiben, eine E-Mail senden) im Hintergrund.
Wiederholungen
Ein fehlgeschlagener Aufruf wird erneut versucht nach 5 Sekunden, 5 Minuten, 30 Minuten, dann nach 2, 5, 10, 14, 20 und 24 Stunden: zehn Versuche über etwa drei Tage. Nach dem letzten wird die Zustellung als Fehlgeschlagen markiert.
Es ist nicht garantiert, dass Aufrufe in Reihenfolge ankommen, und einer kann doppelt ankommen (zum Beispiel nach einem Timeout bei einem Aufruf, den dein Server doch verarbeitet hat). Mach deinen Handler idempotent: Verarbeite anhand von webhook-id, und wenn die Reihenfolge zweier Aufrufe wichtig ist, verlass dich auf die Daten (ihren status, ihre Beträge) statt auf die Ankunftsreihenfolge.
Ein Webhook, der dauerhaft fehlschlägt
Ein Webhook, dessen Aufrufe drei Tage lang ohne einen einzigen Erfolg fehlschlagen, wird abgeschaltet, und der Crew-Eigentümer bekommt eine E-Mail. Die noch wartenden Aufrufe bleiben erhalten. Bring den Server in Ordnung, dann klick auf Einschalten am Webhook: Die Fehler-Uhr beginnt neu, und die wartenden Aufrufe gehen raus. Ereignisse, die passierten, während er abgeschaltet war, wurden nicht für ihn eingereiht: Lies die API einmal, um aufzuholen.
Zustellungen und "Erneut senden"
Zustellungen an jedem Webhook listet seine letzten 50 Aufrufe (Aufrufe werden 30 Tage aufbewahrt): das Ereignis, den Status (Wartet, Wird gesendet, Zugestellt, Fehlgeschlagen, Nicht gesendet), die Anzahl der Versuche und die letzte Antwort. Der Payload selbst wird nicht aufbewahrt, da er zum Sendezeitpunkt gebaut wird.
Erneut senden setzt eine abgeschlossene Zustellung wieder an den Anfang der Warteschlange, mit einem Payload, gebaut aus dem aktuellen Stand und derselben webhook-id. Nicht gesendet heißt, es gab nichts mehr zu senden: Das Objekt wurde gelöscht, oder deine Crew sieht es nicht mehr.
Das Signatur-Secret ändern
Neues Signatur-Secret an einem Webhook legt ein neues Secret an und zeigt es einmal. 24 Stunden lang trägt jeder Aufruf zwei Signaturen in webhook-signature, eine mit dem alten Secret und eine mit dem neuen, damit du das neue Secret ausrollen kannst, ohne einen Aufruf zu verpassen. Nach 24 Stunden signiert das alte Secret nicht mehr.
Rotier es, wann immer das Secret geleakt sein könnte: ein ehemaliger Auftragnehmer, ein öffentlich gemachtes Repository, ein Log, das es ausgegeben hat.
Pausieren, bearbeiten, löschen
- Pausieren stoppt die Aufrufe. Schon wartende Aufrufe bleiben erhalten und gehen raus, sobald du den Webhook wieder einschaltest; Ereignisse, die während der Pause passieren, werden nicht für ihn eingereiht.
- Bearbeiten ändert die Adresse, die Ereignisse, die Beschreibung und die Einstellung für personenbezogene Daten. Das Secret bleibt gleich.
- Löschen entfernt den Webhook, sein Secret und seinen Zustellverlauf. Das lässt sich nicht rückgängig machen.