Webhooks
Un webhook est une adresse sur votre serveur que Sway appelle avec un POST quand quelque chose se passe : une commande est payée, un événement passe en ligne, quelqu'un répond à un formulaire. Votre site n'a plus besoin de demander à l'API toutes les quelques minutes si quelque chose a changé ; il l'apprend en quelques secondes.
Les webhooks se règlent dans l'application web, pas à travers l'API : Admin crew → API → Webhooks. Ils viennent avec l'API, sur les formules Studio et Roster, et la personne qui les règle a besoin de la permission Gérer l'API sur le crew.
Ajouter un webhook
- Ouvrez Admin crew → API, puis l'onglet Webhooks, et cliquez sur Ajouter un webhook.
- Saisissez l'URL de l'endpoint : une adresse
httpspublique sur votre serveur, par exemplehttps://www.example.com/webhooks/sway. - Cochez les événements qu'il doit recevoir (voir la liste plus bas).
- Laissez Inclure les données personnelles désactivé, sauf si l'endpoint a besoin des noms et adresses e-mail (voir Données personnelles).
- Cliquez sur Ajouter un webhook. Le secret de signature apparaît une fois, commençant par
whsec_. Copiez-le dans la configuration de votre serveur : Sway ne le montre plus jamais.
Envoyez-vous tout de suite un appel avec Envoyer un événement de test : un envoi webhook.test arrive en quelques secondes et apparaît sous Envois.
Un crew peut avoir jusqu'à 10 webhooks.
Adresses que Sway n'appellera pas
L'adresse doit utiliser https, nommer un hôte (pas une adresse IP), et résoudre vers une adresse publique au moment où Sway envoie l'appel. Sway refuse localhost, les réseaux privés et link-local, les adresses portant un nom d'utilisateur ou un mot de passe, et les ports en dessous de 1024 autres que 443. Les redirections ne sont pas suivies : pointez le webhook vers l'adresse finale.
Pour recevoir des webhooks sur une machine de développement, exposez-la à travers un tunnel qui vous donne une adresse https publique.
Les événements
| Type | Envoyé quand | Nomme une personne |
|---|---|---|
order.paid | Une commande pour un de vos événements est payée, sur Sway ou via votre site | Oui : l'e-mail de l'acheteur |
order.refunded | Une commande est remboursée, en tout ou en partie (un appel par remboursement) | Oui : l'e-mail de l'acheteur |
event.published | Un événement publié devient visible pour votre crew : il passe en ligne, ou une de vos pages y est ajoutée | Non |
event.updated | Un événement que vous voyez change : date, horaires, titre, description, image, salle, lineup, statut, ou il est dépublié | Non |
booking_request.created | Une demande de booking arrive via POST /v1/booking-requests | Oui : le contact |
ambassador.sale_attributed | Une vente payée est créditée à un de vos ambassadeurs | Oui : le nom de l'ambassadeur |
form.submitted | Quelqu'un remplit un de vos formulaires | Oui : les réponses et le contact |
payout.paid | Un versement atteint le compte bancaire d'une de vos pages | Non |
Qui reçoit quoi suit les mêmes règles que les lectures de l'API :
- Commandes et versements vont aux crews qui gèrent la page vers laquelle va l'argent. Un crew qui ne fait que booker un artiste sur le lineup n'entend pas parler des ventes de billets.
- Événements vont à chaque crew dont une page est sur l'événement : ses promoteurs, sa salle, ses artistes confirmés.
- Ventes ambassadeurs vont aux crews qui gèrent la page du programme.
- Formulaires vont au crew qui possède le formulaire, ou qui gère la page à laquelle il appartient.
Sway vérifie à nouveau juste avant d'envoyer. Un crew qui a perdu la page entre-temps ne reçoit rien.
event.published et event.updated
event.published est l'appel qui dit à votre site d'ajouter un événement. Il est envoyé quand un événement est publié, et aussi, une fois par webhook, quand une de vos pages est ajoutée à un événement déjà publié (l'événement vient de rejoindre votre crew). Le premier appel d'un nouvel événement attend 30 secondes, pour que la salle, la couverture et le reste de la création arrivent dans la même charge utile.
event.updated attend 60 secondes, et les changements faits entre-temps voyagent ensemble : dix modifications rapides ne font qu'un appel. Il est aussi envoyé quand un événement est dépublié ; sa charge utile dit alors seulement "published": false, pour que votre site puisse retirer l'événement.
Quand l'un de ces appels amène votre serveur à relire l'API (l'événement, une liste où il figure), envoyez ces lectures avec Cache-Control: no-cache : Sway les lit alors à la source au lieu de répondre avec une copie faite avant le changement. Voir Mise en cache.
À quoi ressemble un appel
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":{...}}}
Le corps est l'enveloppe Standard Webhooks :
| Champ | Signification |
|---|---|
type | Le type d'événement, tiré du tableau ci-dessus |
timestamp | Quand c'est arrivé, ISO 8601 en UTC |
data | L'objet, lu depuis Sway au moment où l'appel est envoyé, donc toujours l'état courant |
webhook-id identifie l'événement. Il reste le même à chaque nouvelle tentative et sur un Renvoyer manuel : utilisez-le pour ignorer un appel déjà traité.
Charges utiles
Les montants sont des entiers dans la plus petite unité de la devise (centimes), les devises sont des codes ISO 4217.
order.paid et 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 vaut true quand la commande est passée par un paiement que votre site a ouvert avec POST /v1/checkout-sessions. buyer vaut null sur un webhook sans données personnelles. Sur order.refunded, amount_refunded est le total remboursé jusqu'ici.
event.published et 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'événement est le résumé que l'API renvoie dans les listes ; appelez GET /v1/events/{id} pour le lineup et le reste. Un événement dépublié :
{
"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, envoyé par Envoyer un événement de 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" }
}
Données personnelles
Désactivé par défaut, un appel dit ce qui s'est passé sans dire qui : buyer, contact, answers et ambassador valent null. Activez Inclure les données personnelles et ils portent les noms, adresses e-mail et réponses.
Seul le propriétaire du crew peut l'activer, car cela envoie des données personnelles hors de Sway vers un serveur que Sway ne contrôle pas. Un membre détenant la permission Gérer l'API peut le désactiver. Qui que ce soit qui fasse tourner le serveur receveur devient responsable de ces données : ne stockez que ce dont vous avez besoin, et supprimez-le une fois que ce n'est plus le cas.
Vérifiez la signature
Chaque appel est signé avec le secret du webhook, à la manière Standard Webhooks, pour que votre serveur puisse refuser tout ce qui ne vient pas de Sway :
- Prenez le corps brut, exactement tel que reçu. Analyser puis resérialiser le JSON change les octets et casse la signature.
- Construisez la chaîne
{webhook-id}.{webhook-timestamp}.{body}. - Calculez-en un HMAC-SHA256, avec pour clé la partie du secret après
whsec_, décodée en base64. - Comparez-le, encodé en base64, à chaque signature de
webhook-signature. L'en-tête est une liste séparée par des espaces dev1,{signature}; une seule correspondance suffit. Utilisez une comparaison à temps constant. - Refusez un
webhook-timestampà plus de 5 minutes de votre horloge, pour qu'un appel intercepté ne puisse pas être rejoué plus tard.
Sway envoie chaque appel avec un en-tête Content-Length. La plupart des appels pèsent quelques kilo-octets ; une réponse de formulaire avec de longs textes peut dépasser 1 Mo. Refusez un appel sans cet en-tête, ou qui annonce plus que ce que peuvent peser les événements que vous recevez, avant de lire le corps : la signature ne se vérifie qu'une fois le corps lu, et cela empêche un appelant non signé de remplir la mémoire de votre serveur.
Standard Webhooks publie des bibliothèques de vérification pour beaucoup de langages, dont JavaScript, Python, PHP, Go et Ruby (liste). En 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 garde le corps sous forme d'octets : la signature porte sur ces octets.
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() // répondez d'abord, travaillez ensuite
queueSwayEvent(req.headers['webhook-id'], event)
})
La même vérification sans bibliothèque, en 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)
})
}
Et en PHP, pour un site WordPress ou PHP simple :
<?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'] : traitez-les après avoir répondu.
Répondez vite, traitez ensuite
Sway attend 10 secondes une réponse et ne lit que son code de statut. N'importe quel 2xx compte comme livré ; tout le reste, ou aucune réponse du tout, est un échec. Répondez dès que la signature est vérifiée, puis faites le travail (mettre à jour un cache, écrire dans un CRM, envoyer un e-mail) en arrière-plan.
Nouvelles tentatives
Un appel en échec est retenté après 5 secondes, 5 minutes, 30 minutes, puis 2, 5, 10, 14, 20 et 24 heures : dix tentatives sur environ trois jours. Après la dernière, l'envoi est marqué Échec.
Rien ne garantit que les appels arrivent dans l'ordre, et l'un d'eux peut arriver deux fois (après un délai dépassé sur un appel que votre serveur a pourtant traité, par exemple). Rendez votre gestionnaire idempotent : indexez votre traitement sur webhook-id, et quand l'ordre de deux appels compte, fiez-vous aux données (leur status, leurs montants) plutôt qu'à l'ordre d'arrivée.
Un webhook qui continue d'échouer
Un webhook dont les appels échouent depuis trois jours sans un seul succès est désactivé, et le propriétaire du crew reçoit un e-mail. Les appels qu'il avait encore en attente sont gardés. Réparez le serveur, puis cliquez sur Activer sur le webhook : l'horloge des échecs repart de zéro et les appels en attente partent. Les événements survenus pendant qu'il était désactivé n'ont pas été mis en file pour lui : relisez l'API une fois pour vous remettre à jour.
Envois et "Renvoyer"
Envois sur chaque webhook liste ses 50 derniers appels (les appels sont gardés 30 jours) : l'événement, l'état (En attente, En cours, Livré, Échec, Non envoyé), le nombre d'essais et la dernière réponse. La charge utile elle-même n'est pas gardée, puisqu'elle est construite au moment de l'envoi.
Renvoyer replace un envoi terminé au début du planning, avec une charge utile construite à partir de l'état courant et le même webhook-id. Non envoyé veut dire qu'il n'y avait plus rien à envoyer : l'objet a été supprimé, ou votre crew ne le voit plus.
Faites tourner le secret
Nouveau secret de signature sur un webhook crée un nouveau secret et le montre une fois. Pendant 24 heures, chaque appel porte deux signatures dans webhook-signature, une avec l'ancien secret et une avec le nouveau, pour que vous puissiez déployer le nouveau secret sans manquer un appel. Après 24 heures, l'ancien secret arrête de signer.
Faites tourner le secret dès qu'il a pu fuiter : un ancien prestataire, un dépôt rendu public, un log qui l'a affiché.
Mettre en pause, modifier, supprimer
- Mettre en pause arrête les appels. Les appels déjà en attente sont gardés et partent quand vous réactivez le webhook ; les événements survenus pendant la pause ne sont pas mis en file pour lui.
- Modifier change l'adresse, les événements, la description et le réglage des données personnelles. Le secret reste le même.
- Supprimer retire le webhook, son secret et son historique d'envois. C'est irréversible.