Webhooks
Een webhook is een adres op je server dat Sway aanroept met een POST zodra er iets gebeurt: een bestelling wordt betaald, een evenement gaat live, iemand vult een formulier in. Je site hoeft de API niet meer elke paar minuten te vragen of er iets veranderde; ze hoort het binnen enkele seconden.
Webhooks stel je in via de web-app, niet via de API: Crew admin → API → Webhooks. Ze horen bij de API, op de abonnementen Studio en Roster, en wie ze instelt heeft het recht API beheren op de crew nodig.
Voeg een webhook toe
- Open Crew admin → API, dan het tabblad Webhooks, en klik op Webhook toevoegen.
- Vul de Endpoint-URL in: een openbaar
https-adres op je server, bijvoorbeeldhttps://www.example.com/webhooks/sway. - Vink de events aan die hij moet ontvangen (zie de lijst hieronder).
- Laat Persoonsgegevens meesturen uit tenzij het endpoint namen en e-mailadressen nodig heeft (zie Persoonsgegevens).
- Klik op Webhook toevoegen. Het ondertekeningssecret verschijnt één keer, beginnend met
whsec_. Kopieer het naar de configuratie van je server: Sway toont het nooit meer.
Stuur meteen een aanroep naar jezelf met Testevent sturen: een aflevering webhook.test komt binnen enkele seconden binnen en verschijnt onder Afleveringen.
Een crew kan tot 10 webhooks hebben.
Adressen die Sway niet aanroept
Het adres moet https gebruiken, een host noemen (geen IP-adres), en oplossen naar een openbaar adres op het moment dat Sway de aanroep verstuurt. Sway weigert localhost, privé- en link-localnetwerken, adressen met een gebruikersnaam of wachtwoord erin, en poorten onder 1024 behalve 443. Redirects worden niet gevolgd: wijs de webhook naar het uiteindelijke adres.
Om webhooks te ontvangen op een ontwikkelmachine, ontsluit je die via een tunnel die je een openbaar https-adres geeft.
De events
| Type | Verstuurd wanneer | Noemt een persoon |
|---|---|---|
order.paid | Een bestelling voor een van je evenementen wordt betaald, op Sway of via je site | Ja: het e-mailadres van de koper |
order.refunded | Een bestelling wordt terugbetaald, geheel of gedeeltelijk (één aanroep per terugbetaling) | Ja: het e-mailadres van de koper |
event.published | Een gepubliceerd evenement wordt zichtbaar voor je crew: het gaat live, of een van je pagina's wordt eraan toegevoegd | Nee |
event.updated | Een evenement dat je ziet verandert: datum, tijden, titel, beschrijving, afbeelding, locatie, line-up, status, of het gaat offline | Nee |
booking_request.created | Er komt een bookingaanvraag binnen via POST /v1/booking-requests | Ja: het contact |
ambassador.sale_attributed | Een betaalde verkoop wordt toegeschreven aan een van je ambassadeurs | Ja: de naam van de ambassadeur |
form.submitted | Iemand vult een van je formulieren in | Ja: de antwoorden en het contact |
payout.paid | Een uitbetaling bereikt de bankrekening van een van je pagina's | Nee |
Wie wat ontvangt, volgt dezelfde regels als de reads van de API:
- Bestellingen en uitbetalingen gaan naar de crews die de pagina beheren waar het geld naartoe gaat. Een crew die een artiest alleen op de line-up boekt, hoort niets over ticketverkoop.
- Events gaan naar elke crew wiens pagina's op het evenement staan: zijn promotors, zijn locatie, zijn bevestigde artiesten.
- Ambassadeursverkopen gaan naar de crews die de pagina van het programma beheren.
- Formulieren gaan naar de crew die het formulier bezit, of die de pagina beheert waarbij het hoort.
Sway controleert opnieuw vlak voor het versturen. Een crew die de pagina intussen kwijtraakte, krijgt niets.
event.published en event.updated
event.published is de aanroep die je site vertelt een evenement toe te voegen. Hij wordt verstuurd zodra een evenement gepubliceerd wordt, en ook, één keer per webhook, wanneer een van je pagina's wordt toegevoegd aan een evenement dat al gepubliceerd is (het evenement is net bij je crew gekomen). De eerste aanroep van een nieuw evenement wacht 30 seconden, zodat de locatie, de cover en de rest van de aanmaak in dezelfde payload meekomen.
event.updated wacht 60 seconden, en wijzigingen die intussen gebeuren, liften mee: tien snelle bewerkingen maken één aanroep. Hij wordt ook verstuurd wanneer een evenement offline gaat; zijn payload zegt dan alleen "published": false en verder niets, zodat je site het evenement kan verwijderen.
Wanneer een van deze aanroepen je server de API opnieuw laat lezen (het evenement, een lijst waarin het voorkomt), stuur die reads dan met Cache-Control: no-cache: Sway leest ze dan vers in plaats van te antwoorden vanuit een kopie van voor de wijziging. Zie Caching.
Hoe een aanroep eruitziet
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":{...}}}
De body is de envelop van Standard Webhooks:
| Veld | Betekenis |
|---|---|
type | Het eventtype, uit de tabel hierboven |
timestamp | Wanneer het gebeurde, ISO 8601 in UTC |
data | Het object, gelezen uit Sway op het moment dat de aanroep verstuurd wordt, dus altijd de actuele status |
webhook-id identificeert het event. Het blijft hetzelfde bij elke retry en bij een handmatige Opnieuw versturen: gebruik het om een aanroep te negeren die je al verwerkte.
Payloads
Bedragen zijn integers in de kleinste eenheid van de munt (centen), munten zijn ISO 4217-codes.
order.paid en 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 is true wanneer de bestelling liep via een checkout die je site opende met POST /v1/checkout-sessions. buyer is null bij een webhook zonder persoonsgegevens. Bij order.refunded is amount_refunded het totaal dat tot dusver is terugbetaald.
event.published en 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
}
}
}
Het evenement is de samenvatting die de API teruggeeft in lijsten; haal GET /v1/events/{id} op voor de line-up en de rest. Een evenement dat offline gaat:
{
"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, verstuurd door Testevent sturen:
{
"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" }
}
Persoonsgegevens
Standaard uit, een aanroep zegt wat er gebeurde zonder te zeggen wie: buyer, contact, answers en ambassador zijn null. Zet Persoonsgegevens meesturen aan en ze dragen de namen, e-mailadressen en antwoorden.
Alleen de eigenaar van de crew kan het aanzetten, omdat het persoonsgegevens uit Sway stuurt naar een server die Sway niet beheert. Een lid met het recht API beheren kan het uitzetten. Wie de ontvangende server draait, wordt verantwoordelijk voor die gegevens: bewaar alleen wat je nodig hebt, en verwijder het zodra dat niet meer zo is.
Controleer de handtekening
Elke aanroep is ondertekend met het secret van de webhook, op de manier van Standard Webhooks, zodat je server alles kan weigeren wat niet van Sway kwam:
- Neem de raw body, precies zoals ontvangen. De JSON parsen en opnieuw serialiseren verandert de bytes en breekt de handtekening.
- Bouw de string
{webhook-id}.{webhook-timestamp}.{body}. - Bereken er een HMAC-SHA256 van, met als sleutel het deel van het secret na
whsec_, base64-gedecodeerd. - Vergelijk hem, base64-gecodeerd, met elke handtekening in
webhook-signature. De header is een spatiegescheiden lijst vanv1,{signature}; één match volstaat. Gebruik een constant-time vergelijking. - Weiger een
webhook-timestampdie meer dan 5 minuten van je klok afwijkt, zodat een onderschepte aanroep niet later opnieuw afgespeeld kan worden.
Sway stuurt elke aanroep met een Content-Length-header. De meeste aanroepen wegen een paar kilobyte; een formulierantwoord met lange teksten kan boven 1 MB uitkomen. Weiger een aanroep zonder die header, of met meer dan de events die je ontvangt kunnen wegen, voordat je de body leest: de handtekening is pas te controleren als de body gelezen is, en zo kan een niet-ondertekende aanroeper het geheugen van je server niet vullen.
Standard Webhooks publiceert verificatiebibliotheken voor veel talen, waaronder JavaScript, Python, PHP, Go en Ruby (lijst). 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 houdt de body als bytes: de handtekening geldt over die 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() // eerst antwoorden, dan pas het werk
queueSwayEvent(req.headers['webhook-id'], event)
})
Dezelfde controle zonder library, 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)
})
}
En in PHP, voor een WordPress- of gewone 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']: verwerk het na het antwoorden.
Antwoord snel, verwerk daarna
Sway wacht 10 seconden op een antwoord en leest alleen de statuscode. Elke 2xx telt als afgeleverd; al de rest, of helemaal geen antwoord, is een mislukking. Antwoord zodra de handtekening klopt, en doe het werk (een cache bijwerken, schrijven naar een CRM, een e-mail sturen) daarna op de achtergrond.
Retries
Een mislukte aanroep wordt opnieuw geprobeerd na 5 seconden, 5 minuten, 30 minuten, dan na 2, 5, 10, 14, 20 en 24 uur: tien pogingen over ongeveer drie dagen. Na de laatste wordt de aflevering gemarkeerd als Mislukt.
Aanroepen komen niet gegarandeerd in volgorde binnen, en één kan twee keer aankomen (bijvoorbeeld na een timeout op een aanroep die je server toch verwerkte). Maak je handler idempotent: baseer de verwerking op webhook-id, en wanneer de volgorde van twee aanroepen ertoe doet, vertrouw dan op de gegevens (zijn status, zijn bedragen) in plaats van op de volgorde van aankomst.
Een webhook die blijft mislukken
Een webhook waarvan de aanroepen drie dagen lang mislukken zonder één succes wordt uitgeschakeld, en de crew-eigenaar krijgt een e-mail. De aanroepen die nog wachtten, blijven bewaard. Herstel je server en klik dan op Aanzetten bij de webhook: de mislukkingsklok begint opnieuw en de wachtende aanroepen gaan eruit. Events die gebeurden terwijl hij uit stond, werden niet voor hem in de wachtrij gezet: lees de API eenmalig opnieuw om bij te komen.
Afleveringen en "Opnieuw versturen"
Afleveringen op elke webhook toont zijn laatste 50 aanroepen (aanroepen blijven 30 dagen bewaard): het event, de status (Wacht, Wordt verstuurd, Afgeleverd, Mislukt, Niet verstuurd), het aantal pogingen en het laatste antwoord. De payload zelf wordt niet bewaard, want die wordt opgebouwd op het moment van versturen.
Opnieuw versturen zet een afgeronde aflevering terug aan het begin van het schema, met een payload opgebouwd uit de huidige status en hetzelfde webhook-id. Niet verstuurd betekent dat er niets meer te versturen was: het object werd verwijderd, of je crew ziet het niet meer.
Roteer het secret
Nieuw ondertekeningssecret op een webhook maakt een nieuw secret aan en toont het één keer. Gedurende 24 uur draagt elke aanroep twee handtekeningen in webhook-signature, één met het oude secret en één met het nieuwe, zodat je het nieuwe secret kunt uitrollen zonder een aanroep te missen. Na 24 uur ondertekent het oude secret niet meer.
Roteer telkens wanneer het secret gelekt kan zijn: een voormalige freelancer, een repository die publiek werd, een log die het afdrukte.
Pauzeren, bewerken, verwijderen
- Pauzeren stopt de aanroepen. Aanroepen die al wachtten, blijven bewaard en gaan eruit zodra je de webhook weer aanzet; events die tijdens de pauze gebeuren, worden niet voor hem in de wachtrij gezet.
- Bewerken wijzigt het adres, de events, de beschrijving en de instelling voor persoonsgegevens. Het secret blijft hetzelfde.
- Verwijderen verwijdert de webhook, zijn secret en zijn afleveringsgeschiedenis. Dit kan niet ongedaan gemaakt worden.