Webhook

Sway chiama il tuo server nel momento in cui succede qualcosa, con una firma così puoi verificare che venga da Sway.

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

  1. Apri Admin della crew → API, poi la scheda Webhook, e clicca Aggiungi webhook.
  2. Inserisci l'URL dell'endpoint: un indirizzo https pubblico sul tuo server, per esempio https://www.example.com/webhooks/sway.
  3. Spunta gli eventi che deve ricevere (vedi l'elenco più sotto).
  4. Lascia spento Includi dati personali a meno che l'endpoint non abbia bisogno di nomi e indirizzi email (vedi Dati personali).
  5. 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

TipoInviato quandoNomina una persona
order.paidUn ordine per uno dei tuoi eventi viene pagato, su Sway o tramite il tuo sitoSì: l'email dell'acquirente
order.refundedUn ordine viene rimborsato, in tutto o in parte (una chiamata per ogni rimborso)Sì: l'email dell'acquirente
event.publishedUn evento pubblicato diventa visibile alla tua crew: va online, oppure una delle tue pagine viene aggiunta a essoNo
event.updatedUn evento che vedi cambia: data, orari, titolo, descrizione, immagine, locale, line-up, stato, oppure viene tolto dalla pubblicazioneNo
booking_request.createdArriva una richiesta di booking tramite POST /v1/booking-requestsSì: il contatto
ambassador.sale_attributedUna vendita pagata viene accreditata a uno dei tuoi ambassadorSì: il nome dell'ambassador
form.submittedQualcuno completa uno dei tuoi moduliSì: le risposte e il contatto
payout.paidUn pagamento raggiunge il conto bancario di una delle tue pagineNo

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:

CampoSignificato
typeIl tipo di evento, dalla tabella sopra
timestampQuando è successo, ISO 8601 in UTC
dataL'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:

  1. Prendi il corpo grezzo, esattamente come ricevuto. Fare il parsing e riserializzare il JSON cambia i byte e rompe la firma.
  2. Costruisci la stringa {webhook-id}.{webhook-timestamp}.{body}.
  3. Calcolane un HMAC-SHA256, con chiave la parte del segreto dopo whsec_, decodificata da base64.
  4. Confrontala, codificata in base64, con ogni firma in webhook-signature. L'header è un elenco separato da spazi di v1,{signature}; una corrispondenza basta. Usa un confronto a tempo costante.
  5. Rifiuta un webhook-timestamp distante 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.
Non saltare mai il controllo, nemmeno dietro un URL segreto. Senza, chiunque scopra l'indirizzo può inviare al tuo sito ordini falsi.

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.