Webhook
Un webhook è un indirizzo sul tuo server che Sway chiama con un POST quando succede qualcosa: un ordine viene pagato, un evento va online, qualcuno risponde a un modulo. Il tuo sito non deve più chiedere all'API ogni pochi minuti se qualcosa è cambiato: lo viene a sapere entro pochi secondi.
I webhook si configurano nell'app web, non tramite l'API: Admin della crew → API → Webhook. Vengono con l'API, sui piani Studio e Roster, e chi li configura ha bisogno del permesso Gestire API sulla crew.
Aggiungi un webhook
- Apri Admin della crew → API, poi la scheda Webhook, e clicca Aggiungi webhook.
- Inserisci l'URL dell'endpoint: un indirizzo
httpspubblico sul tuo server, per esempiohttps://www.example.com/webhooks/sway. - Spunta gli eventi che deve ricevere (vedi l'elenco più sotto).
- Lascia spento Includi dati personali a meno che l'endpoint non abbia bisogno di nomi e indirizzi email (vedi Dati personali).
- Clicca Aggiungi webhook. Il segreto di firma compare una volta, iniziando con
whsec_. Copialo nella configurazione del tuo server: Sway non lo mostrerà più.
Inviati subito una chiamata con Invia un evento di test: un invio webhook.test arriva entro pochi secondi e compare sotto Invii.
Una crew può avere fino a 10 webhook.
Indirizzi che Sway non chiamerà
L'indirizzo deve usare https, nominare un host (non un indirizzo IP), e risolvere a un indirizzo pubblico quando Sway invia la chiamata. Sway rifiuta localhost, le reti private e link-local, gli indirizzi con un nome utente o una password al loro interno, e le porte sotto 1024 tranne la 443. I redirect non vengono seguiti: punta il webhook all'indirizzo finale.
Per ricevere webhook su una macchina di sviluppo, esponila tramite un tunnel che ti dia un indirizzo https pubblico.
Gli eventi
| Tipo | Inviato quando | Nomina una persona |
|---|---|---|
order.paid | Un ordine per uno dei tuoi eventi viene pagato, su Sway o tramite il tuo sito | Sì: l'email dell'acquirente |
order.refunded | Un ordine viene rimborsato, in tutto o in parte (una chiamata per ogni rimborso) | Sì: l'email dell'acquirente |
event.published | Un evento pubblicato diventa visibile alla tua crew: va online, oppure una delle tue pagine viene aggiunta a esso | No |
event.updated | Un evento che vedi cambia: data, orari, titolo, descrizione, immagine, locale, line-up, stato, oppure viene tolto dalla pubblicazione | No |
booking_request.created | Arriva una richiesta di booking tramite POST /v1/booking-requests | Sì: il contatto |
ambassador.sale_attributed | Una vendita pagata viene accreditata a uno dei tuoi ambassador | Sì: il nome dell'ambassador |
form.submitted | Qualcuno completa uno dei tuoi moduli | Sì: le risposte e il contatto |
payout.paid | Un pagamento raggiunge il conto bancario di una delle tue pagine | No |
Chi riceve cosa segue le stesse regole delle letture dell'API:
- Ordini e pagamenti vanno alle crew che gestiscono la pagina a cui vanno i soldi. Una crew che si limita a prenotare un artista in line-up non viene a sapere delle vendite di biglietti.
- Eventi vanno a ogni crew le cui pagine sono sull'evento: i suoi promoter, il suo locale, i suoi artisti confermati.
- Vendite ambassador vanno alle crew che gestiscono la pagina del programma.
- Moduli vanno alla crew che possiede il modulo, o che gestisce la pagina a cui appartiene.
Sway verifica di nuovo appena prima di inviare. Una crew che nel frattempo ha perso la pagina non riceve nulla.
event.published e event.updated
event.published è la chiamata che dice al tuo sito di aggiungere un evento. Viene inviata quando un evento è pubblicato, e anche, una volta per webhook, quando una delle tue pagine viene aggiunta a un evento già pubblicato (l'evento è appena entrato nella tua crew). La prima chiamata di un evento nuovo aspetta 30 secondi, così il locale, la copertina e il resto della creazione arrivano nello stesso payload.
event.updated aspetta 60 secondi, e i cambiamenti fatti nel frattempo viaggiano insieme: dieci modifiche rapide fanno una sola chiamata. Viene inviata anche quando un evento viene tolto dalla pubblicazione; il suo payload allora dice solo "published": false e nient'altro, così il tuo sito può rimuovere l'evento.
Quando una di queste chiamate porta il tuo server a rileggere l'API (l'evento, un elenco in cui compare), invia quelle letture con Cache-Control: no-cache: Sway le legge allora alla fonte invece di rispondere con una copia fatta prima del cambiamento. Vedi Cache.
Come appare una chiamata
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":{...}}}
Il corpo è la busta Standard Webhooks:
| Campo | Significato |
|---|---|
type | Il tipo di evento, dalla tabella sopra |
timestamp | Quando è successo, ISO 8601 in UTC |
data | L'oggetto, letto da Sway nel momento in cui la chiamata viene inviata, quindi è sempre lo stato attuale |
webhook-id identifica l'evento. Resta lo stesso a ogni nuovo tentativo e su un Invia di nuovo manuale: usalo per ignorare una chiamata che hai già elaborato.
Payload
Gli importi sono numeri interi nell'unità più piccola della valuta (centesimi), le valute sono codici ISO 4217.
order.paid e 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 è true quando l'ordine è passato da un checkout che il tuo sito ha aperto con POST /v1/checkout-sessions. buyer è null su un webhook senza dati personali. Su order.refunded, amount_refunded è il totale rimborsato finora.
event.published e 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
}
}
}
L'evento è il riassunto che l'API restituisce negli elenchi; recupera GET /v1/events/{id} per la line-up e il resto. Un evento tolto dalla pubblicazione:
{
"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, inviato da Invia un evento di test:
{
"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" }
}
Dati personali
Spento di default, una chiamata dice cosa è successo senza dire chi: buyer, contact, answers e ambassador sono null. Attiva Includi dati personali e portano nomi, indirizzi email e risposte.
Solo il proprietario della crew può attivarlo, perché invia dati personali fuori da Sway verso un server che Sway non controlla. Un membro con il permesso Gestire API può disattivarlo. Chi gestisce il server ricevente diventa responsabile di quei dati: conserva solo quello che ti serve, ed eliminalo quando non ti serve più.
Verifica la firma
Ogni chiamata è firmata con il segreto del webhook, alla maniera di Standard Webhooks, così il tuo server può rifiutare qualsiasi cosa non provenga da Sway:
- Prendi il corpo grezzo, esattamente come ricevuto. Fare il parsing e riserializzare il JSON cambia i byte e rompe la firma.
- Costruisci la stringa
{webhook-id}.{webhook-timestamp}.{body}. - Calcolane un HMAC-SHA256, con chiave la parte del segreto dopo
whsec_, decodificata da base64. - Confrontala, codificata in base64, con ogni firma in
webhook-signature. L'header è un elenco separato da spazi div1,{signature}; una corrispondenza basta. Usa un confronto a tempo costante. - Rifiuta un
webhook-timestampdistante più di 5 minuti dal tuo orologio, così una chiamata intercettata non può essere ripetuta più tardi.
Sway invia ogni chiamata con un'intestazione Content-Length. La maggior parte delle chiamate pesa pochi kilobyte; una risposta a un modulo con testi lunghi può superare 1 MB. Prima di leggere il corpo, rifiuta una chiamata senza quell'intestazione, o che dichiara più di quanto possano pesare gli eventi che ricevi: la firma si può verificare solo dopo aver letto il corpo, e così chi chiama senza firma non può riempire la memoria del tuo server.
Standard Webhooks pubblica librerie di verifica per molti linguaggi, tra cui JavaScript, Python, PHP, Go e Ruby (elenco). 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 mantiene il corpo come byte: la firma è calcolata su quei byte.
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() // rispondi prima, lavora dopo
queueSwayEvent(req.headers['webhook-id'], event)
})
La stessa verifica senza libreria, 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)
})
}
E in PHP, per WordPress o un sito PHP semplice:
<?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']: elaborali dopo aver risposto.
Rispondi in fretta, elabora dopo
Sway aspetta 10 secondi una risposta e legge solo il suo codice di stato. Qualsiasi 2xx conta come consegnato; qualsiasi altra cosa, o nessuna risposta, è un fallimento. Rispondi non appena la firma è verificata, poi fai il lavoro (aggiornare una cache, scrivere in un CRM, inviare un'email) in background.
Nuovi tentativi
Una chiamata fallita viene ritentata dopo 5 secondi, 5 minuti, 30 minuti, poi 2, 5, 10, 14, 20 e 24 ore: dieci tentativi in circa tre giorni. Dopo l'ultimo, l'invio è segnato Fallito.
Non è garantito che le chiamate arrivino in ordine, e una può arrivare due volte (per esempio dopo un timeout su una chiamata che il tuo server aveva già elaborato). Rendi il tuo handler idempotente: basa l'elaborazione su webhook-id, e quando l'ordine di due chiamate conta, affidati ai dati (il loro status, i loro importi) piuttosto che all'ordine di arrivo.
Un webhook che continua a fallire
Un webhook le cui chiamate hanno fallito per tre giorni senza un solo successo viene disattivato, e il proprietario della crew riceve un'email. Le chiamate che aveva ancora in attesa vengono conservate. Sistema il server, poi clicca Attiva sul webhook: l'orologio dei fallimenti riparte e le chiamate in attesa partono. Gli eventi successi mentre era spento non sono stati messi in coda per lui: rileggi l'API una volta per recuperare.
Invii e "Invia di nuovo"
Invii su ogni webhook elenca le sue ultime 50 chiamate (le chiamate si conservano 30 giorni): l'evento, lo stato (In attesa, In invio, Consegnato, Fallito, Non inviato), il numero di tentativi e l'ultima risposta. Il payload in sé non viene conservato, dato che è costruito al momento dell'invio.
Invia di nuovo rimette un invio concluso all'inizio della schedulazione, con un payload costruito dallo stato attuale e lo stesso webhook-id. Non inviato significa che non c'era più niente da inviare: l'oggetto è stato eliminato, oppure la tua crew non lo vede più.
Ruota il segreto
Nuovo segreto di firma su un webhook crea un nuovo segreto e lo mostra una volta. Per 24 ore ogni chiamata porta due firme in webhook-signature, una con il vecchio segreto e una con il nuovo, così puoi distribuire il nuovo segreto senza perdere una chiamata. Dopo 24 ore il vecchio segreto smette di firmare.
Ruota il segreto ogni volta che potrebbe essere trapelato: un ex collaboratore, un repository reso pubblico, un log che lo ha stampato.
Metti in pausa, modifica, elimina
- Metti in pausa ferma le chiamate. Le chiamate già in attesa vengono conservate e partono quando riattivi il webhook; gli eventi che succedono durante la pausa non vengono messi in coda per lui.
- Modifica cambia l'indirizzo, gli eventi, la descrizione e l'impostazione dei dati personali. Il segreto resta lo stesso.
- Elimina rimuove il webhook, il suo segreto e la sua cronologia degli invii. Non si può annullare.