Webhooks

Sway llama a tu servidor en cuanto pasa algo, con una firma para que compruebes que viene de Sway.

Un webhook es una dirección de tu servidor a la que Sway llama con un POST cuando pasa algo: se paga un pedido, un evento se publica, alguien responde a un formulario. Tu web ya no tiene que preguntarle a la API cada pocos minutos si algo ha cambiado: se entera en segundos.

Los webhooks se configuran en la aplicación web, no a través de la API: Admin del crew → API → Webhooks. Vienen con la API, en los planes Studio y Roster, y quien los configura necesita el permiso Gestionar API en el crew.


Añade un webhook

  1. Abre Admin del crew → API, luego la pestaña Webhooks, y haz clic en Añadir webhook.
  2. Introduce la URL del endpoint: una dirección https pública en tu servidor, por ejemplo https://www.example.com/webhooks/sway.
  3. Marca los eventos que debe recibir (ver la lista más abajo).
  4. Deja Incluir datos personales desactivado salvo que el endpoint necesite nombres y direcciones de email (ver Datos personales).
  5. Haz clic en Añadir webhook. El secreto de firma aparece una vez, empezando por whsec_. Cópialo en la configuración de tu servidor: Sway no lo vuelve a mostrar nunca.

Envíate una llamada al momento con Enviar un evento de prueba: un envío webhook.test llega en segundos y aparece en Envíos.

Un crew puede tener hasta 10 webhooks.

Direcciones a las que Sway no llamará

La dirección debe usar https, nombrar un host (no una dirección IP), y resolver a una dirección pública cuando Sway envía la llamada. Sway rechaza localhost, las redes privadas y link-local, las direcciones con un nombre de usuario o una contraseña dentro, y los puertos por debajo de 1024 que no sean el 443. Las redirecciones no se siguen: apunta el webhook a la dirección final.

Para recibir webhooks en una máquina de desarrollo, expónla a través de un túnel que te dé una dirección https pública.


Los eventos

TipoSe envía cuandoNombra a una persona
order.paidSe paga un pedido de uno de tus eventos, en Sway o a través de tu webSí: el email del comprador
order.refundedSe reembolsa un pedido, total o parcialmente (una llamada por reembolso)Sí: el email del comprador
event.publishedUn evento publicado se vuelve visible para tu crew: se publica, o una de tus páginas se añade a élNo
event.updatedCambia un evento que ves: fecha, horarios, título, descripción, imagen, sala, line-up, estado, o se despublicaNo
booking_request.createdLlega una solicitud de booking a través de POST /v1/booking-requestsSí: el contacto
ambassador.sale_attributedSe atribuye una venta pagada a uno de tus embajadoresSí: el nombre del embajador
form.submittedAlguien completa uno de tus formulariosSí: las respuestas y el contacto
payout.paidUn pago llega a la cuenta bancaria de una de tus páginasNo

Quién recibe qué sigue las mismas reglas que las lecturas de la API:

  • Pedidos y pagos van a los crews que gestionan la página a la que llega el dinero. Un crew que solo tiene reservado a un artista en el line-up no se entera de las ventas de entradas.
  • Eventos van a todos los crews cuyas páginas están en el evento: sus promotores, su sala, sus artistas confirmados.
  • Ventas de embajadores van a los crews que gestionan la página del programa.
  • Formularios van al crew propietario del formulario, o que gestiona la página a la que pertenece.

Sway lo vuelve a comprobar justo antes de enviar. Un crew que perdió la página mientras tanto no recibe nada.

event.published y event.updated

event.published es la llamada que le dice a tu web que añada un evento. Se envía cuando un evento se publica, y también, una vez por webhook, cuando una de tus páginas se añade a un evento que ya está publicado (el evento acaba de unirse a tu crew). La primera llamada de un evento nuevo espera 30 segundos, para que la sala, la portada y el resto de la creación lleguen en el mismo payload.

event.updated espera 60 segundos, y los cambios hechos mientras tanto viajan juntos: diez ediciones rápidas hacen una sola llamada. También se envía cuando un evento se despublica; su payload entonces dice "published": false y nada más, para que tu web pueda quitar el evento.

Cuando una de estas llamadas hace que tu servidor vuelva a leer la API (el evento, una lista en la que aparece), envía esas lecturas con Cache-Control: no-cache: Sway las lee entonces frescas en vez de responder desde una copia anterior al cambio. Ver Caché.


Cómo es una llamada

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":{...}}}

El cuerpo es el sobre de Standard Webhooks:

CampoSignificado
typeEl tipo de evento, de la tabla de arriba
timestampCuándo pasó, ISO 8601 en UTC
dataEl objeto, leído de Sway en el momento de enviar la llamada, así que siempre es el estado actual

webhook-id identifica el evento. Se mantiene igual en cada reintento y en un Reenviar manual: úsalo para ignorar una llamada que ya has procesado.

Payloads

Los importes son enteros en la unidad más pequeña de la moneda (céntimos), las monedas son códigos ISO 4217.

order.paid y 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 es true cuando el pedido pasó por un checkout que tu web abrió con POST /v1/checkout-sessions. buyer es null en un webhook sin datos personales. En order.refunded, amount_refunded es el total reembolsado hasta el momento.

event.published y 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
    }
  }
}

El evento es el resumen que la API devuelve en las listas; pide GET /v1/events/{id} para el line-up y el resto. Un evento despublicado:

{
  "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, enviado por Enviar un evento de prueba:

{
  "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" }
}

Datos personales

Desactivado por defecto, una llamada dice qué pasó sin decir quién: buyer, contact, answers y ambassador son null. Activa Incluir datos personales y pasan a llevar los nombres, las direcciones de email y las respuestas.

Solo el propietario del crew puede activarlo, porque envía datos personales fuera de Sway a un servidor que Sway no controla. Un miembro con el permiso Gestionar API puede desactivarlo. Quien administre el servidor receptor se vuelve responsable de esos datos: guarda solo lo que necesites, y bórralo en cuanto dejes de necesitarlo.


Comprueba la firma

Toda llamada se firma con el secreto del webhook, a la manera de Standard Webhooks, para que tu servidor pueda rechazar cualquier cosa que no venga de Sway:

  1. Toma el cuerpo en bruto, exactamente como llega. Analizar y volver a serializar el JSON cambia los bytes y rompe la firma.
  2. Construye la cadena {webhook-id}.{webhook-timestamp}.{body}.
  3. Calcula un HMAC-SHA256 de ella, con clave la parte del secreto después de whsec_, decodificada en base64.
  4. Compárala, codificada en base64, con cada firma de webhook-signature. La cabecera es una lista separada por espacios de v1,{signature}; con una coincidencia basta. Usa una comparación de tiempo constante.
  5. Rechaza un webhook-timestamp a más de 5 minutos de tu reloj, para que una llamada interceptada no se pueda reproducir más tarde.

Sway envía cada llamada con una cabecera Content-Length. La mayoría de las llamadas pesan pocos kilobytes; una respuesta de formulario con textos largos puede superar 1 MB. Antes de leer el cuerpo, rechaza una llamada sin esa cabecera, o que declare más de lo que pueden pesar los eventos que recibes: la firma solo se puede comprobar una vez leído el cuerpo, y así quien llama sin firmar no puede llenar la memoria de tu servidor.

Standard Webhooks publica librerías de verificación para muchos lenguajes, entre ellos JavaScript, Python, PHP, Go y Ruby (lista). 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 conserva el cuerpo como bytes: la firma es sobre esos 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() // responde primero, trabaja después
  queueSwayEvent(req.headers['webhook-id'], event)
})

La misma comprobación sin librería, 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)
  })
}

Y en PHP, para un sitio WordPress o PHP normal:

<?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']: procésalo después de responder.
Nunca te saltes la comprobación, ni siquiera detrás de una URL secreta. Sin ella, cualquiera que averigüe la dirección puede enviar a tu web pedidos falsos.

Responde rápido, procesa después

Sway espera 10 segundos una respuesta y solo lee su código de estado. Cualquier 2xx cuenta como entregado; cualquier otra cosa, o ninguna respuesta, es un fallo. Responde en cuanto la firma sea correcta, y haz el trabajo (actualizar una caché, escribir en un CRM, enviar un email) en segundo plano.

Reintentos

Una llamada fallida se reintenta a los 5 segundos, 5 minutos, 30 minutos, y luego a las 2, 5, 10, 14, 20 y 24 horas: diez intentos a lo largo de unos tres días. Después del último, el envío se marca como Fallido.

No hay garantía de que las llamadas lleguen en orden, y una puede llegar dos veces (por ejemplo, tras un timeout en una llamada que tu servidor sí procesó). Haz que tu manejador sea idempotente: basa el procesamiento en webhook-id, y cuando el orden de dos llamadas importe, apóyate en los datos (su status, sus importes) y no en el orden de llegada.

Un webhook que sigue fallando

Un webhook cuyas llamadas han fallado durante tres días sin un solo éxito se desactiva, y el propietario del crew recibe un email. Las llamadas que aún tenía en espera se conservan. Arregla el servidor, y luego haz clic en Activar en el webhook: el reloj de fallos vuelve a empezar y las llamadas en espera salen. Los eventos que pasaron mientras estaba desactivado no se encolaron para él: lee la API una vez para ponerte al día.

Envíos y "Reenviar"

Envíos, en cada webhook, lista sus últimas 50 llamadas (las llamadas se conservan 30 días): el evento, el estado (En espera, Enviando, Entregado, Fallido, No enviado), el número de intentos y la última respuesta. El payload en sí no se conserva, ya que se construye en el momento de enviar.

Reenviar pone un envío terminado de vuelta al principio de la cola, con un payload construido a partir del estado actual y el mismo webhook-id. No enviado significa que ya no había nada que enviar: el objeto se eliminó, o tu crew ya no lo ve.


Cambia el secreto

Nuevo secreto de firma, en un webhook, crea un secreto nuevo y lo muestra una vez. Durante 24 horas toda llamada lleva dos firmas en webhook-signature, una con el secreto antiguo y otra con el nuevo, para que puedas desplegar el secreto nuevo sin perder ninguna llamada. Pasadas 24 horas, el secreto antiguo deja de firmar.

Cámbialo cuando el secreto pueda haberse filtrado: un antiguo colaborador, un repositorio que se hizo público, un log que lo imprimió.


Pausa, edita, elimina

  • Pausar detiene las llamadas. Las que ya estaban en espera se conservan y salen cuando vuelves a activar el webhook; los eventos que pasan durante la pausa no se encolan para él.
  • Editar cambia la dirección, los eventos, la descripción y el ajuste de datos personales. El secreto no cambia.
  • Eliminar borra el webhook, su secreto y su historial de envíos. No se puede deshacer.