Webhooks

Sway roept je server aan zodra er iets gebeurt, ondertekend zodat je kunt controleren dat het van Sway komt.

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

  1. Open Crew admin → API, dan het tabblad Webhooks, en klik op Webhook toevoegen.
  2. Vul de Endpoint-URL in: een openbaar https-adres op je server, bijvoorbeeld https://www.example.com/webhooks/sway.
  3. Vink de events aan die hij moet ontvangen (zie de lijst hieronder).
  4. Laat Persoonsgegevens meesturen uit tenzij het endpoint namen en e-mailadressen nodig heeft (zie Persoonsgegevens).
  5. 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

TypeVerstuurd wanneerNoemt een persoon
order.paidEen bestelling voor een van je evenementen wordt betaald, op Sway of via je siteJa: het e-mailadres van de koper
order.refundedEen bestelling wordt terugbetaald, geheel of gedeeltelijk (één aanroep per terugbetaling)Ja: het e-mailadres van de koper
event.publishedEen gepubliceerd evenement wordt zichtbaar voor je crew: het gaat live, of een van je pagina's wordt eraan toegevoegdNee
event.updatedEen evenement dat je ziet verandert: datum, tijden, titel, beschrijving, afbeelding, locatie, line-up, status, of het gaat offlineNee
booking_request.createdEr komt een bookingaanvraag binnen via POST /v1/booking-requestsJa: het contact
ambassador.sale_attributedEen betaalde verkoop wordt toegeschreven aan een van je ambassadeursJa: de naam van de ambassadeur
form.submittedIemand vult een van je formulieren inJa: de antwoorden en het contact
payout.paidEen uitbetaling bereikt de bankrekening van een van je pagina'sNee

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:

VeldBetekenis
typeHet eventtype, uit de tabel hierboven
timestampWanneer het gebeurde, ISO 8601 in UTC
dataHet 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:

  1. Neem de raw body, precies zoals ontvangen. De JSON parsen en opnieuw serialiseren verandert de bytes en breekt de handtekening.
  2. Bouw de string {webhook-id}.{webhook-timestamp}.{body}.
  3. Bereken er een HMAC-SHA256 van, met als sleutel het deel van het secret na whsec_, base64-gedecodeerd.
  4. Vergelijk hem, base64-gecodeerd, met elke handtekening in webhook-signature. De header is een spatiegescheiden lijst van v1,{signature}; één match volstaat. Gebruik een constant-time vergelijking.
  5. Weiger een webhook-timestamp die 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.
Sla de controle nooit over, ook niet achter een geheim URL. Zonder de controle kan iedereen die het adres kent, valse bestellingen naar je site sturen.

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.