Referencia de endpoints

Todos los endpoints de la API pública de Sway v1, agrupados por recurso, con sus permisos, parámetros y la forma de la respuesta.

Todos los endpoints viven bajo https://www.sway.events/api/v1 y quedan limitados al crew dueño de la clave de API, enviada como token Bearer:

curl https://www.sway.events/api/v1/artists \
  -H "Authorization: Bearer sway_sk_YOUR_KEY"

Los endpoints de lista devuelven el sobre estándar { data, pagination } y paginación por cursor; todo endpoint de un solo recurso devuelve el objeto del recurso directamente, sin envoltorio data. Consulta Paginación y filtros para los cursores, los filtros y el mecanismo expand.

Toda respuesta 4xx o 5xx es un problema RFC 9457 (application/problem+json) con un campo code estable. Consulta Autenticación para el ciclo de vida de la clave, los permisos, los límites de peticiones y la tabla completa de códigos de error.

Esta página documenta los 80 endpoints que trabajan con los datos propios de un crew. El servidor MCP y los webhooks son mecanismos de entrega aparte sobre los mismos datos y tienen sus propias páginas; consulta la sección final.


Clave y crew

GET /v1/me

Introspección de la clave: la prueba de "¿sigue viva mi clave?". Devuelve la clave identificada (id, tipo, permisos, límite de peticiones y límites de trabajo, sitios web registrados) y su crew.

No requiere permiso. Claves publicables: no. Nunca en caché (la respuesta depende de la clave y siempre debe estar actualizada).

{
  "key": {
    "id": "3f9a1c02-7b45-4c8e-9d21-0a1b2c3d4e5f",
    "type": "secret",
    "scopes": ["read:profile", "read:artists", "read:promoters", "read:events", "read:venues", "read:content"],
    "rate_limit": { "per_minute": 120, "per_day": 20000 },
    "limits": {
      "requests_per_minute": 2400,
      "crew_work_per_minute": 600,
      "crew_work_per_day": 120000,
      "bytes_per_day": 5368709120
    },
    "websites": []
  },
  "crew": { "id": "a2628d53-4f21-4c1e-9b7d-1a2b3c4d5e6f", "name": "Insomnia", "slug": "insomnia" }
}

rate_limit es el presupuesto de trabajo propio de la clave (lo que gasta un fallo de caché); limits son los techos más amplios descritos en la página de Autenticación. Sin errores propios de este endpoint más allá de los de autenticación estándar.

GET /v1/usage

Lo que la clave que llama ha gastado de sus límites ahora mismo: trabajo en este minuto y hoy, el techo de peticiones, los bytes servidos hoy, y si la clave está congelada tras un patrón inusual de peticiones.

No requiere permiso. Claves publicables: no. Nunca en caché; leerlo nunca gasta trabajo (coste 0).

{
  "key": { "id": "3f9a1c02-7b45-4c8e-9d21-0a1b2c3d4e5f", "type": "secret" },
  "plan": "studio",
  "work": {
    "minute": { "limit": 120, "used": 12, "remaining": 108, "reset_seconds": 41 },
    "day": { "limit": 20000, "used": 340, "remaining": 19660, "reset_seconds": 61200 }
  },
  "crew_work": {
    "minute": { "limit": 600, "used": 12, "remaining": 588, "reset_seconds": 41 },
    "day": { "limit": 120000, "used": 340, "remaining": 119660, "reset_seconds": 61200 }
  },
  "requests": {
    "minute": { "limit": 2400, "used": 15, "remaining": 2385, "reset_seconds": 41 }
  },
  "bytes": { "today": 1048576, "limit": 5368709120 },
  "frozen": false
}

work es el presupuesto propio de la clave; crew_work es el mismo presupuesto compartido entre todas las claves del crew. Sin errores propios de este endpoint.

GET /v1/audit-log

Las últimas llamadas hechas con la clave, la más reciente primero: hora, método, ruta, estado, dirección de quien llama, resultado de caché y duración. Se guardan siete días, 1.000 entradas como máximo.

No requiere permiso. Claves publicables: no. Nunca en caché (privacy: no-store).

ParámetroValoresPor defectoSignificado
limitentero, 1 a 1000100Cuántas entradas devolver
{
  "data": [
    {
      "at": "2026-09-26T12:00:03.000Z",
      "method": "GET",
      "path": "/api/v1/events/4521",
      "status": 200,
      "ip": "203.0.113.7",
      "request_id": "9f2c1a7e-4d3b-4e21-8c7a-1b2c3d4e5f60",
      "cache": "hit",
      "ms": 4
    }
  ],
  "count": 1
}

ip es la dirección observada de la clave que llama. cache es hit, miss, stale o null cuando el endpoint no se puede cachear.

Errores: validation_error (400) cuando limit está fuera de rango.

POST /v1/keys/rotate

Sustituye la clave que llama por una nueva: mismo nombre, tipo, permisos, límites y sitios web. La clave antigua sigue funcionando durante un periodo de gracia (para que un socio pueda desplegar de nuevo sin cortes), y después caduca por sí sola.

Permiso write:keys (se activa aparte). Claves publicables: no. Nunca en caché. No acepta Idempotency-Key: la respuesta lleva el nuevo secreto, y Sway nunca guarda una respuesta que lleve un secreto, así que no hay nada que repetir en un reintento.

CampoTipoObligatorioNotas
grace_hoursnumbernoNúmero entero, de 1 a 168, 24 por defecto: cuánto tiempo sigue respondiendo la clave antigua
{
  "key": "sway_sk_ab12...",
  "record": {
    "id": "7c5e2b41-...",
    "name": "Storefront key",
    "key_prefix": "sway_sk_ab12",
    "last4": "wxyz",
    "key_type": "secret",
    "scopes": ["read:artists", "read:events"],
    "storefront_origins": [],
    "created_at": "2026-09-26T12:00:00.000Z",
    "expires_at": null,
    "rotated_from_id": "3f9a1c02-7b45-4c8e-9d21-0a1b2c3d4e5f"
  },
  "previous": { "id": "3f9a1c02-7b45-4c8e-9d21-0a1b2c3d4e5f", "expires_at": "2026-09-27T12:00:00.000Z" }
}

El nuevo secreto completo (key) se muestra una vez, en esta respuesta, igual que al crearla: guárdalo de inmediato.

Errores: validation_error (400, cuerpo incorrecto o grace_hours inválido), invalid_key (401, la clave ya no existía), conflict (409, esta clave ya tiene una sucesora: rota la clave que la reemplazó en su lugar), rate_limited (429, una clave solo puede rotar una vez por hora).

GET /v1/crew

El perfil del crew más los ids de las páginas que gestiona.

Permiso read:profile. Claves publicables: sí. En caché 300 s.

{
  "id": "a2628d53-4f21-4c1e-9b7d-1a2b3c4d5e6f",
  "name": "Insomnia",
  "slug": "insomnia",
  "pages": { "artists": [245, 512], "promoters": [697] }
}

Sin errores propios de este endpoint.

GET /v1/crew/pages

Las páginas que gestiona el crew (artistas, promotores, lugares), cada una con su nombre y si está publicada. A diferencia de cualquier otro endpoint, aquí sí aparece una página en borrador: es la lista propia del crew.

Permiso read:profile. Claves publicables: no. En caché 300 s.

{
  "data": [
    { "type": "artist", "id": 245, "name": "KROMATIEK", "image_url": "https://assets.sway.events/artists/245/cover.webp", "is_published": true, "page_url": "https://www.sway.events/artist/245" },
    { "type": "promoter", "id": 697, "name": "Insomnia Nights", "image_url": null, "is_published": false, "page_url": null }
  ]
}

image_url y page_url son null mientras la página siga sin publicarse. Sin errores propios de este endpoint.

GET /v1/genres

La taxonomía de géneros: ids y nombres estables, ordenados por nombre. Estos ids son los que aceptan los filtros genre= de GET /v1/artists, GET /v1/events y GET /v1/events/search.

No requiere permiso. Claves publicables: sí. En caché 3.600 s.

{
  "data": [
    { "id": 12, "name": "Hard Groove" },
    { "id": 45, "name": "Techno" }
  ]
}

Sin errores propios de este endpoint.


Descubrimiento

Estos cuatro endpoints no necesitan ninguna clave de API: describen la propia API. Cada uno tiene un límite de 60 peticiones por minuto y dirección de quien llama, y responde 429 rate_limited a partir de ahí.

GET /v1/openapi.json

Esta API como documento OpenAPI 3.1, generado a partir de la misma lista de endpoints sobre la que corre la API, así que nunca describe un endpoint que no existe ni omite uno que sí.

No requiere clave. En caché 3.600 s.

Sin errores propios de este endpoint más allá del límite por dirección de arriba.

GET /v1/scopes

Todos los permisos que puede tener una clave: qué concede cada uno, si una clave publicable puede tenerlo, si solo el propietario del crew puede concederlo, y qué endpoints abre.

No requiere clave. En caché 3.600 s.

{
  "data": [
    {
      "scope": "read:events",
      "group": "read",
      "default": true,
      "publishable": true,
      "owner_only": false,
      "summary": "Read published events, their lineups, days, tickets on sale and availability.",
      "routes": ["GET /v1/events", "GET /v1/events/:id", "GET /v1/events/:id/ticket-tiers"]
    },
    {
      "scope": "write:checkout:free",
      "group": "write",
      "default": false,
      "publishable": false,
      "owner_only": true,
      "summary": "Issue free (comp) orders. The crew owner grants it.",
      "routes": ["POST /v1/checkout-sessions"]
    }
  ]
}

La respuesta real lista los diecisiete permisos y todos los endpoints que abre cada uno.

GET /v1/changelog

Qué cambió en la API, lo más reciente primero, cada fecha con los endpoints que salieron ese día.

No requiere clave. En caché 3.600 s.

{
  "data": [
    {
      "date": "2026-08-01",
      "changes": ["Stripe checkout sessions for tickets, and newsletter sign-up."],
      "routes": ["POST /v1/checkout-sessions", "POST /v1/newsletter-signup"]
    }
  ]
}

GET /v1/status

Si la API y aquello de lo que depende responden: la base de datos, los pagos (Stripe) y el email, cada uno ok o down, comprobados como mucho una vez por intervalo para que el propio endpoint nunca se convierta en carga. El conjunto es ok solo cuando todos los componentes lo son.

No requiere clave. Nunca en caché.

{
  "status": "ok",
  "api_version": "v1",
  "time": "2026-09-26T12:00:00.000Z",
  "components": { "database": "ok", "payments": "ok", "email": "ok" }
}

Nunca una cifra de latencia, una versión ni un mensaje de error: solo ok o down.


Artistas

Todos los endpoints de abajo aceptan una clave publicable.

GET /v1/artists

El roster: todos los artistas que gestiona el crew, por nombre, ascendente. Los artistas del roster se devuelven aunque no estén publicados, ya que el roster son datos propios del crew.

Permiso read:artists. En caché 300 s.

ParámetroValoresPor defectoSignificado
qtexto, de 2 a 100 caracteresningunoEl nombre contiene esto, comparado como texto
genrelista de ids de género separados por comas, 10 como máximoningunoCualquiera de estos géneros
updated_sincefecha o fecha y hora ISO 8601ningunoCambiado en este instante o después
idslista de ids separados por comas, 50 como máximoningunoSolo estos artistas
limitentero, 1 a 10025Tamaño de página
cursorcadena opacaningunoDel next_cursor de una página anterior
{
  "data": [
    {
      "id": 245,
      "name": "KROMATIEK",
      "image_url": "https://assets.sway.events/artists/245/cover.webp",
      "description": "Ghent-based techno artist.",
      "genres": ["Techno", "Hard Groove"],
      "links": { "spotify": "https://open.spotify.com/artist/xyz", "instagram": "https://instagram.com/kromatiek" },
      "is_verified": true,
      "managed": true,
      "contact_emails": [{ "type": "booking", "value": "[email protected]" }]
    }
  ],
  "pagination": { "next_cursor": "eyJrIjoiS1JPTUFUSUVLIiwiaWQiOjI0NX0", "has_more": true, "limit": 25 }
}

contact_emails es solo del nivel gestionado y nunca aparece dentro de links. Errores: validation_error (400, q, genre, updated_since, ids o limit incorrectos), invalid_cursor (400).

GET /v1/artists/{id}

Un artista del roster, representación completa, devuelto directamente. Un artista ajeno (un invitado del line-up que no gestiona el crew) no se puede pedir así.

Permiso read:artists. En caché 300 s.

Misma forma que una entrada del roster de arriba. Errores: not_found (404, id mal formado, id desconocido, o un artista que el crew no gestiona).

GET /v1/artists/{id}/events

El historial de actuaciones y el calendario del artista: eventos publicados donde el artista es miembro confirmado del line-up. Cada elemento lleva una referencia ligera venue.

Permisos read:artists y read:events (ambos obligatorios). En caché 120 s.

ParámetroValoresPor defectoSignificado
statusupcoming, past, allupcomingQué eventos incluir
limitentero, 1 a 10025Tamaño de página
cursorcadena opacaningunoDe una página anterior

Este feed no acepta from, to ni expand.

{
  "data": [
    {
      "id": 4521,
      "title": "Overload",
      "description": "A night of hard groove.",
      "starts_at": "2026-09-12T20:00:00.000Z",
      "ends_at": "2026-09-13T04:00:00.000Z",
      "timezone": "Europe/Brussels",
      "image_url": "https://assets.sway.events/events/4521/cover.webp",
      "type": "club",
      "page_url": "https://www.sway.events/event/4521",
      "venue": { "id": 88, "name": "Jungle Bar" }
    }
  ],
  "pagination": { "next_cursor": null, "has_more": false, "limit": 25 }
}

Errores: not_found (404, id mal formado o artista que el crew no gestiona), validation_error (400, status incorrecto), invalid_cursor (400).

GET /v1/artists/{id}/ics

Las próximas fechas del artista como feed iCalendar (text/calendar) al que suscribirse. 100 eventos como máximo, RFC 5545, líneas plegadas a 75 octetos, horas en UTC. GET /v1/promoters/{id}/ics y GET /v1/events/{id}/ics, más abajo, son el mismo feed para los eventos de un promotor y para un solo evento.

Permisos read:artists y read:events (ambos obligatorios). En caché 300 s.

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Sway//Sway API v1//EN
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:KROMATIEK
BEGIN:VEVENT
UID:[email protected]
DTSTAMP:20260926T120000Z
DTSTART:20260912T200000Z
DTEND:20260913T040000Z
SUMMARY:Overload
LOCATION:Jungle Bar\, Ghent
URL:https://www.sway.events/event/4521
END:VEVENT
END:VCALENDAR

El archivo se llama artist-{id}.ics (Content-Disposition: inline). Errores: not_found (404, id mal formado, o el artista no está gestionado o no está publicado).

GET /v1/artists/{id}/presskits

Los press kits públicos del artista, el que es por defecto primero. Aquí se dejan fuera los kits privados y los borradores; consulta GET /v1/presskits para la lista completa propia del crew.

Permisos read:artists y read:content (ambos obligatorios). En caché 300 s.

{
  "artist_id": 245,
  "data": [
    {
      "id": "1b2c3d4e-5f60-4a1b-9c2d-3e4f5a6b7c8d",
      "artist_id": 245,
      "title": "KROMATIEK 2026 EPK",
      "status": "public",
      "protected": false,
      "is_default": true,
      "updated_at": "2026-09-01T10:00:00.000Z",
      "url": "https://www.sway.events/artist/245/presskit/1b2c3d4e-5f60-4a1b-9c2d-3e4f5a6b7c8d"
    }
  ]
}

Errores: not_found (404, id mal formado o artista que el crew no gestiona).


Promotores

Todos los endpoints de abajo aceptan una clave publicable.

GET /v1/promoters

Todos los promotores que gestiona el crew, representación completa, ordenados por nombre.

Permiso read:promoters. En caché 300 s.

ParámetroValoresPor defectoSignificado
qtexto, de 2 a 100 caracteresningunoEl nombre contiene esto
idslista de ids separados por comas, 50 como máximoningunoSolo estos promotores
limitentero, 1 a 10025Tamaño de página
cursorcadena opacaningunoDe una página anterior

Mismo sobre y misma forma de elemento que GET /v1/artists (campos de promotor en vez de campos de artista; sin filtro de género). Errores: validation_error (400), invalid_cursor (400).

GET /v1/promoters/{id}

Un promotor gestionado y publicado, representación completa, devuelto directamente.

Permiso read:promoters. En caché 300 s.

Errores: not_found (404, id mal formado o promotor que el crew no gestiona).

GET /v1/promoters/{id}/events

Eventos publicados de un promotor gestionado: el feed principal sobre el que se construye la web de un socio.

Permisos read:promoters y read:events (ambos obligatorios). En caché 60 s.

ParámetroValoresPor defectoSignificado
statusupcoming, past, allupcomingQué eventos
from / tofecha o fecha y hora ISO 8601ningunoEmpieza en este instante o después / o antes
expandlista separada por comas de venue, lineup, ticket_tiersningunoIncluir recursos relacionados
limitentero, 1 a 10025Tamaño de página
cursorcadena opacaningunoDe una página anterior
{
  "data": [
    {
      "id": 4521,
      "title": "Overload",
      "description": "A night of hard groove.",
      "starts_at": "2026-09-12T20:00:00.000Z",
      "ends_at": "2026-09-13T04:00:00.000Z",
      "timezone": "Europe/Brussels",
      "image_url": "https://assets.sway.events/events/4521/cover.webp",
      "type": "club",
      "page_url": "https://www.sway.events/event/4521",
      "venue": { "id": 88, "name": "Jungle Bar" }
    }
  ],
  "pagination": { "next_cursor": null, "has_more": false, "limit": 25 }
}

venue es una referencia ligera { id, name } por defecto; expand=venue la eleva al objeto completo de nivel público, y expand=lineup / expand=ticket_tiers añaden esos arrays a cada elemento (mismas formas que en GET /v1/events/{id}). Errores: validation_error (400, status, from, to o expand incorrectos), invalid_cursor (400), not_found (404, id mal formado o promotor que el crew no gestiona).

GET /v1/promoters/{id}/ics

Los próximos eventos del promotor como feed iCalendar, misma forma que GET /v1/artists/{id}/ics (ver Artistas), descargado como promoter-{id}.ics.

Permisos read:promoters y read:events (ambos obligatorios). En caché 300 s.

Errores: not_found (404, id mal formado o promotor que el crew no gestiona).

GET /v1/promoters/{id}/venues

Los lugares donde se celebran los eventos publicados del promotor, el más usado primero.

Permisos read:promoters y read:venues (ambos obligatorios). En caché 300 s.

{
  "data": [
    {
      "id": 88,
      "name": "Jungle Bar",
      "image_url": "https://assets.sway.events/venues/88/cover.webp",
      "description": "A cellar club in Ghent.",
      "location": { "address": "Nieuwewandeling 2, Ghent" },
      "genres": [],
      "links": null,
      "capacity": { "max": 300, "layouts": { "standing": 300, "seated": 120 } },
      "event_count": 14
    }
  ]
}

Errores: not_found (404, id mal formado o promotor que el crew no gestiona).

GET /v1/promoters/{id}/artists

Los artistas que el promotor ha contratado en sus eventos publicados, el más contratado primero. Los artistas del roster llegan en el nivel completo, el resto en el nivel público.

Permisos read:promoters y read:artists (ambos obligatorios). En caché 300 s.

{
  "data": [
    { "id": 245, "name": "KROMATIEK", "image_url": "https://assets.sway.events/artists/245/cover.webp", "description": "Ghent-based techno artist.", "genres": ["Techno"], "links": null, "is_verified": true, "managed": true, "contact_emails": [], "booking_count": 6 },
    { "id": 1042, "name": "CE$AR", "image_url": null, "description": null, "genres": ["House"], "links": null, "is_verified": false, "managed": false, "booking_count": 2 }
  ]
}

booking_count cuenta eventos, no sets: una doble reserva la misma noche cuenta una sola vez. Errores: not_found (404, id mal formado o promotor que el crew no gestiona).


Eventos

Las horas de los sets llevan el desfase propio del evento.start_time y end_time en una entrada del line-up, y cada hora dentro de GET /v1/events/{id}/days y GET /v1/events/{id}/timetable, se dejan como RFC 3339 con el desfase UTC propio del evento, por ejemplo 22:00:00+02:00 en vez de 22:00:00+00:00: los dígitos son exactamente lo que escribió el organizador, y el instante es el correcto. Son null mientras el organizador mantiene el timetable desactivado (event-days.ts, apiSetTime).GET /v1/events/{id}/days sigue los días que el organizador declaró en los ajustes de días de festival, o trata el evento entero como un solo día cuando no se ha declarado ninguno; unscheduled guarda los sets sin hora, o los que caen fuera de todos los días declarados. GET /v1/events/{id}/timetable sigue el orden de escenarios propio del organizador, los escenarios no declarados después en orden alfabético, y deja los sets sin escenario al final.

Todos los endpoints de abajo aceptan una clave publicable.

GET /v1/events

El feed unión del grafo del crew: los eventos de sus promotores, las actuaciones confirmadas de su roster, y la agenda de los lugares que gestiona. Cada elemento es un resumen ligero con una referencia venue.

Permiso read:events. En caché 60 s.

ParámetroValoresPor defectoSignificado
statusupcoming, past, allupcomingQué eventos
from / tofecha o fecha y hora ISO 8601ningunoVentana de fechas
promoter_identero positivoningunoUn promotor gestionado
artist_identero positivoningunoUn artista del roster
venue_identero positivoningunoUn lugar del grafo
genrelista de ids de género separados por comas, 10 como máximoningunoCualquiera de estos géneros
citytexto, de 1 a 100 caracteresningunoNombre exacto de la ciudad
countryISO 3166-1 alfa-2ningunoCódigo de país
nearlat,lng en grados decimalesningunoCentro de una búsqueda por radio
radius_kmnúmero, 1 a 50025Radio alrededor de near (necesita near)
updated_sincefecha o fecha y hora ISO 8601ningunoCambiado en este instante o después
has_ticketstrue, falseningunoSi el evento tiene algún tipo de entrada
idslista de ids separados por comas, 50 como máximoningunoSolo estos eventos
limitentero, 1 a 10025Tamaño de página
cursorcadena opacaningunoDe una página anterior

Este feed no admite expand: enviarlo devuelve validation_error (400). Usa GET /v1/events/{id} o GET /v1/promoters/{id}/events para datos expandidos.

{
  "data": [
    {
      "id": 4521,
      "title": "Overload",
      "description": "A night of hard groove.",
      "starts_at": "2026-09-12T20:00:00.000Z",
      "ends_at": "2026-09-13T04:00:00.000Z",
      "timezone": "Europe/Brussels",
      "image_url": "https://assets.sway.events/events/4521/cover.webp",
      "type": "club",
      "page_url": "https://www.sway.events/event/4521",
      "updated_at": "2026-09-10T09:00:00.000Z",
      "venue": { "id": 88, "name": "Jungle Bar" }
    }
  ],
  "pagination": { "next_cursor": "eyJrIjoiMjAyNi0wOS0xMlQyMDowMDowMC4wMDBaIiwiaWQiOjQ1MjF9", "has_more": false, "limit": 25 }
}

Errores: validation_error (400, se envió expand, valor de filtro incorrecto, o radius_km sin near), invalid_cursor (400), not_found (404, promoter_id o artist_id nombra una página que el crew no gestiona).

GET /v1/events/search

El mismo feed que GET /v1/events, filtrado a los eventos cuyo título, descripción o line-up confirmado contiene el texto buscado.

Permiso read:events. En caché 60 s.

Todos los filtros de GET /v1/events, más:

ParámetroValoresPor defectoSignificado
qtexto, de 2 a 100 caracteresobligatorioComparado como texto contra el título, la descripción y el line-up

Mismo sobre que GET /v1/events. Errores: validation_error (400, falta q o tiene una longitud incorrecta, o cualquiera de los filtros compartidos), invalid_cursor (400), not_found (404, promoter_id o artist_id fuera del grafo).

GET /v1/events/{id}

Un evento publicado del grafo del crew, devuelto directamente. venue (nivel público completo) y lineup van siempre incluidos; genres está siempre presente.

Permiso read:events. En caché 60 s.

ParámetroValoresPor defectoSignificado
expandticket_tiersningunovenue y lineup ya van incluidos, así que este es el único valor que añade algo

Cuando está presente expand=ticket_tiers, la respuesta también lleva un objeto ticket_fee: { "percent": number, "fixed": number, "applies_to": "ticket" | "order" }, la comisión de servicio para el beneficiario de este evento, en EUR. Es el mismo objeto que GET /v1/events/{id}/fees, que explica cómo aplicarla.

{
  "id": 4521,
  "title": "Overload",
  "description": "A night of hard groove.",
  "starts_at": "2026-09-12T20:00:00.000Z",
  "ends_at": "2026-09-13T04:00:00.000Z",
  "timezone": "Europe/Brussels",
  "image_url": "https://assets.sway.events/events/4521/cover.webp",
  "type": "club",
  "page_url": "https://www.sway.events/event/4521",
  "venue": {
    "id": 88,
    "name": "Jungle Bar",
    "image_url": "https://assets.sway.events/venues/88/cover.webp",
    "description": "A cellar club in Ghent.",
    "location": { "address": "Nieuwewandeling 2, Ghent" },
    "genres": [],
    "links": null
  },
  "lineup": [
    {
      "stage": "Main", "start_time": "2026-09-12T23:00:00+02:00", "end_time": "2026-09-13T01:00:00+02:00",
      "artist_id": 245, "id": 245, "name": "KROMATIEK", "image_url": "https://assets.sway.events/artists/245/cover.webp",
      "description": "Ghent-based techno artist.", "genres": ["Techno"], "links": { "spotify": "https://open.spotify.com/artist/xyz" },
      "is_verified": true, "managed": true, "contact_emails": [{ "type": "booking", "value": "[email protected]" }]
    },
    {
      "stage": null, "start_time": null, "end_time": null,
      "artist_id": 1042, "id": 1042, "name": "CE$AR", "image_url": null, "description": null,
      "genres": ["House"], "links": null, "is_verified": false, "managed": false
    }
  ],
  "genres": ["Techno"]
}

Las entradas del line-up llevan el DTO propio del artista más stage, start_time, end_time y artist_id: los artistas del roster llegan en el nivel completo (con contact_emails), los invitados ajenos en el nivel público. Un acto personalizado, no vinculado, aparece como una entrada marcador con artist_id: null y su nombre escrito bajo name. La segunda entrada del line-up de arriba tiene start_time/end_time: null porque ese set no tiene hora registrada, no porque el timetable esté desactivado; consulta la nota al principio de la sección.

Errores: validation_error (400, valor de expand desconocido), not_found (404, id mal formado, evento no publicado, o un evento fuera del grafo del crew).

GET /v1/events/{id}/ticket-tiers

Los tipos de entrada del evento: nombre, descripción, precio, estado, periodo de venta y un enlace directo checkout_url al checkout de Sway. Sin recuentos de stock, sin pedidos, sin datos de compradores. Una lista pequeña y fija: envuelta en { "data": [...] } pero sin pagination ni cursor.

Permiso read:events. En caché 30 s.

{
  "data": [
    {
      "id": "301", "name": "Early Bird", "description": "Limited first release", "price": 15.0, "currency": "EUR",
      "fee_amount": 0.75, "price_with_fee": 15.75, "fee_applies_to": "ticket", "status": "sold_out", "sale_start": "2026-06-01T10:00:00.000Z", "sale_end": "2026-07-01T10:00:00.000Z",
      "max_per_order": 4, "checkout_url": "https://www.sway.events/event/4521?ref=api"
    },
    {
      "id": "302", "name": "Regular", "description": null, "price": 20.0, "currency": "EUR", "status": "on_sale",
      "fee_amount": 0.9, "price_with_fee": 20.9, "fee_applies_to": "ticket",
      "sale_start": "2026-07-01T10:00:00.000Z", "sale_end": "2026-09-12T20:00:00.000Z",
      "max_per_order": 6, "checkout_url": "https://www.sway.events/event/4521?ref=api"
    }
  ]
}

status se calcula en el servidor: on_sale, scheduled, sold_out u off_sale. currency es EUR por defecto. Errores: not_found (404).

fee_amount es la comisión de servicio de una entrada y price_with_fee el precio total que paga el comprador (price más fee_amount), según el modelo de comisión del promotor que cobra, las mismas cifras que la página del evento y el MCP público. Muestra primero price_with_fee y fee_amount como comisión de servicio. En el modelo antiguo por pedido (fee_applies_to: "order") ambos son null: la comisión depende de todo el carrito, consulta GET /v1/events/{id}/fees. Los tipos gratuitos tienen un fee_amount de 0.

GET /v1/events/{id}/availability

Si se puede comprar ahora mismo: a la venta, programado, agotado o terminado, por tipo y para el evento en conjunto, con un indicador de "quedan pocas". Nunca un recuento de stock.

Permiso read:events. En caché 30 s.

{
  "event_id": 4521,
  "status": "on_sale",
  "next_sale_start": null,
  "tiers": [
    { "id": "301", "name": "Early Bird", "status": "sold_out", "few_left": false, "sale_start": "2026-06-01T10:00:00.000Z", "sale_end": "2026-07-01T10:00:00.000Z" },
    { "id": "302", "name": "Regular", "status": "on_sale", "few_left": true, "sale_start": "2026-07-01T10:00:00.000Z", "sale_end": "2026-09-12T20:00:00.000Z" }
  ]
}

few_left se activa en cuanto el stock restante de un tipo cae a una décima parte o menos de su stock inicial (mínimo 5), y solo mientras está on_sale. El status propio del evento puede ser no_tickets cuando el evento no tiene ningún tipo de entrada. Errores: not_found (404).

GET /v1/events/{id}/fees

La comisión de servicio que paga un comprador por las entradas de este evento, según el modelo de comisiones del promotor beneficiario: por defecto un porcentaje del precio de cada entrada pagada más una cantidad fija por entrada.

Permiso read:events. En caché 300 s.

{ "event_id": 4521, "percent": 0.03, "fixed": 0.3, "applies_to": "ticket" }

Con applies_to: "ticket", cada entrada pagada lleva round(price * percent + fixed) redondeado al céntimo (un pack cuenta como una entrada, una entrada gratuita no lleva nada), y total = subtotal + la suma de esas comisiones. Algunos promotores conservan un modelo más antiguo, applies_to: "order": total = subtotal + subtotal * percent + fixed, una vez por pedido pagado. En ambos casos el resultado coincide con el total que cobra el checkout de Stripe. Si no se puede resolver el beneficiario, cae en la tarifa estándar por entrada en vez de fallar. Errores: not_found (404).

GET /v1/events/{id}/days

Los días que el organizador declaró en los ajustes de días de festival del evento, cada uno con los sets que empiezan en él; o el evento como un solo día cuando no se ha declarado ninguno.

Permiso read:events. En caché 60 s.

{
  "event_id": 4521,
  "timezone": "Europe/Brussels",
  "timetable_published": true,
  "data": [
    {
      "name": "Friday",
      "date": "2026-09-12",
      "starts_at": "2026-09-12T20:00:00+02:00",
      "ends_at": "2026-09-13T06:00:00+02:00",
      "slots": [
        { "name": "KROMATIEK", "artist_id": 245, "artists": [{ "id": 245, "name": "KROMATIEK", "managed": true }], "stage": "Main", "start_time": "2026-09-12T23:00:00+02:00", "end_time": "2026-09-13T01:00:00+02:00" }
      ]
    }
  ],
  "unscheduled": []
}

unscheduled guarda los sets que no tienen hora, o los que caen fuera de todos los días declarados. Con el timetable desactivado, timetable_published es false y cada día llega con un array slots vacío. Un set B2B es un solo slot con varios artists; name es el nombre personalizado del organizador cuando se ha definido, o si no los artistas unidos con " B2B ". Errores: not_found (404).

GET /v1/events/{id}/timetable

Los sets con hora por escenario, los escenarios en el orden propio que declaró el organizador (el resto en orden alfabético, los que no tienen escenario al final), cada escenario en orden de paso.

Permiso read:events. En caché 60 s.

{
  "event_id": 4521,
  "timezone": "Europe/Brussels",
  "published": true,
  "stages": [
    {
      "stage": "Main",
      "slots": [
        { "name": "KROMATIEK", "artist_id": 245, "artists": [{ "id": 245, "name": "KROMATIEK", "managed": true }], "stage": "Main", "start_time": "2026-09-12T23:00:00+02:00", "end_time": "2026-09-13T01:00:00+02:00" }
      ]
    }
  ]
}

Un set sin hora no está en ningún timetable y se deja fuera por completo (aún puede aparecer en GET /v1/events/{id}/days, bajo unscheduled). Con el timetable desactivado, published es false y stages está vacío. Errores: not_found (404).

GET /v1/events/{id}/ics

El evento como feed iCalendar, misma forma que GET /v1/artists/{id}/ics (ver Artistas), para un botón "añadir al calendario". Descargado como event-{id}.ics.

Permiso read:events. En caché 300 s.

Errores: not_found (404, id mal formado, evento no publicado o fuera del grafo del crew, o el evento no tiene fecha de inicio).

GET /v1/events/{id}/schema-org

El evento como JSON-LD de schema.org (MusicEvent), listo para inyectar tal cual en el <script type="application/ld+json"> de una página de socio, pensado para buscadores.

Permiso read:events. En caché 300 s.

{
  "@context": "https://schema.org",
  "@type": "MusicEvent",
  "name": "Overload",
  "url": "https://www.sway.events/event/4521",
  "eventStatus": "https://schema.org/EventScheduled",
  "eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
  "description": "A night of hard groove.",
  "startDate": "2026-09-12T20:00:00.000Z",
  "endDate": "2026-09-13T04:00:00.000Z",
  "image": ["https://assets.sway.events/events/4521/cover.webp"],
  "location": {
    "@type": "Place",
    "name": "Jungle Bar",
    "address": { "@type": "PostalAddress", "streetAddress": "Nieuwewandeling 2, Ghent", "addressLocality": "Ghent", "addressCountry": "BE" }
  },
  "performer": [{ "@type": "PerformingGroup", "name": "KROMATIEK" }, { "@type": "PerformingGroup", "name": "CE$AR" }],
  "organizer": { "@type": "Organization", "name": "Insomnia Nights", "url": "https://www.sway.events/promoter/697" },
  "offers": [{ "@type": "Offer", "name": "Regular", "price": 20.0, "priceCurrency": "EUR", "availability": "https://schema.org/InStock", "url": "https://www.sway.events/event/4521?ref=api", "validFrom": "2026-07-01T10:00:00.000Z" }]
}

performer se construye a partir de los sets del timetable (no del line-up en bruto), así que lista cada nombre una sola vez y sigue la misma regla de "timetable desactivado" que los endpoints de arriba. Errores: not_found (404).

GET /v1/events/{id}/gallery

Los álbumes de fotos públicos del evento, cada uno con las fotos que terminaron de subirse (las primeras 200) y cuántas tiene en total.

Permiso read:events. En caché 300 s.

{
  "event_id": 4521,
  "data": [
    {
      "id": 12, "name": "Warm-up", "description": null,
      "photos": [{ "id": 501, "url": "https://assets.sway.events/events/4521/gallery/501.webp", "width": 1600, "height": 1067 }],
      "photo_count": 84
    }
  ]
}

Los álbumes privados y las fotos que aún se están subiendo nunca aparecen. Errores: not_found (404).

GET /v1/events/{id}/partners

Los partners de los promotores y lugares del evento, en orden de visualización: misma forma que GET /v1/partners (ver Contenido), envueltos bajo { "event_id": 4521, "data": [...] }.

Permisos read:events y read:content (ambos obligatorios). En caché 300 s.

Errores: not_found (404, evento no publicado o fuera del grafo del crew).

GET /v1/events/{id}/news

Las noticias publicadas del evento, la más reciente primero, en markdown y en HTML saneado: misma forma que GET /v1/news (ver Contenido), envueltas bajo event_id.

Permisos read:events y read:content (ambos obligatorios). En caché 120 s.

ParámetroValoresPor defectoSignificado
limitentero, 1 a 10025Tamaño de página
cursorcadena opacaningunoDe una página anterior

Errores: not_found (404, evento no publicado o fuera del grafo del crew), invalid_cursor (400).

GET /v1/events/{id}/presales

Las preventas del evento que no son borradores, exactamente como las muestra la página pública de preventa: teaser, ventana de registro, si el registro está abierto ahora mismo.

Permiso read:events. En caché 60 s.

{
  "event_id": 4521,
  "data": [
    {
      "token": "f4c8b1a2-...", "status": "open", "registration_open": true,
      "registration_opens_at": "2026-08-01T09:00:00.000Z", "registration_closes_at": "2026-09-05T23:59:00.000Z",
      "featured": true, "featured_mode": "homepage",
      "teaser": { "title": "Early access", "subtitle": "First 100 only", "image_url": null, "description": "Register for a chance at the first release." },
      "url": "https://www.sway.events/presale/f4c8b1a2-..."
    }
  ]
}

Los productos, los topes, la regla de selección, el promotor y la lista de correo detrás de una preventa nunca se exponen. Errores: not_found (404).


Lugares

Todos los endpoints de abajo aceptan una clave publicable.

GET /v1/venues

Los lugares del grafo del crew: los que gestiona (managed: true) y los que acogen sus eventos publicados, nivel público, por nombre.

Permiso read:venues. En caché 300 s.

ParámetroValoresPor defectoSignificado
qtexto, de 2 a 100 caracteresningunoEl nombre contiene esto
idslista de ids separados por comas, 50 como máximoningunoSolo estos lugares
limitentero, 1 a 10025Tamaño de página
cursorcadena opacaningunoDe una página anterior
{
  "data": [
    {
      "id": 88, "name": "Jungle Bar", "image_url": "https://assets.sway.events/venues/88/cover.webp",
      "description": "A cellar club in Ghent.", "location": { "address": "Nieuwewandeling 2, Ghent" },
      "genres": [], "links": null,
      "capacity": { "max": 300, "layouts": { "standing": 300, "seated": 120 } },
      "managed": true
    }
  ],
  "pagination": { "next_cursor": null, "has_more": false, "limit": 25 }
}

Errores: validation_error (400), invalid_cursor (400).

GET /v1/venues/{id}

Un lugar del grafo del crew, nivel público, con su capacidad, devuelto directamente. Solo accesible cuando el crew lo gestiona o acoge un evento publicado del grafo.

Permiso read:venues. En caché 300 s.

Misma forma de elemento que arriba (con managed). Errores: not_found (404).

GET /v1/venues/{id}/events

La agenda del lugar: todos los eventos publicados del grafo del crew que se celebran ahí (todos cuando el crew gestiona el lugar). Mismos filtros que GET /v1/events, sin los filtros de página/artista/lugar.

Permisos read:venues y read:events (ambos obligatorios). En caché 60 s.

ParámetroValoresPor defectoSignificado
statusupcoming, past, allupcomingQué eventos
from / tofecha o fecha y hora ISO 8601ningunoVentana de fechas
limitentero, 1 a 10025Tamaño de página
cursorcadena opacaningunoDe una página anterior

Mismo sobre y misma forma de elemento que GET /v1/events. Errores: not_found (404, id mal formado o lugar fuera del grafo), validation_error (400), invalid_cursor (400).

GET /v1/venues/{id}/capacity

Cuántas personas caben en el lugar según cada distribución de sala (standing, seated, theatre, cabaret, banquet, cocktail, classroom), y la cifra más alta: las distribuciones son montajes alternativos de la misma sala, nunca zonas que se suman.

Permiso read:venues. En caché 300 s.

{ "venue_id": 88, "max": 300, "layouts": { "standing": 300, "seated": 120 } }

max y layouts son null y {} cuando el lugar no indica capacidad. Errores: not_found (404).


Contenido

Todos los endpoints de abajo aceptan una clave publicable.

GET /v1/news

Las noticias publicadas de una página que gestiona el crew (un promotor, un lugar o un artista), la más reciente primero, en markdown y en HTML saneado. Una publicación programada para más tarde se queda fuera hasta su hora. GET /v1/events/{id}/news (ver Eventos) es el mismo feed para un evento en vez de una página.

Permiso read:content. En caché 120 s.

ParámetroValoresPor defectoSignificado
promoter_id / venue_id / artist_identero positivoningunoExactamente uno, que nombre una página que el crew gestiona
limitentero, 1 a 10025Tamaño de página
cursorcadena opacaningunoDe una página anterior
{
  "data": [
    {
      "id": 88, "title": "Lineup announced",
      "body_markdown": "The full lineup is **out now**.",
      "body_html": "<p>The full lineup is <strong>out now</strong>.</p>",
      "hero_image_url": "https://assets.sway.events/news/88/hero.webp",
      "published_at": "2026-08-20T09:00:00.000Z"
    }
  ],
  "pagination": { "next_cursor": null, "has_more": false, "limit": 25 }
}

Errores: validation_error (400, no se dio exactamente uno de promoter_id / venue_id / artist_id), invalid_cursor (400), not_found (404, la página nombrada no la gestiona el crew).

GET /v1/partners

Los partners de un promotor o lugar que gestiona el crew, en orden de visualización.

Permiso read:content. En caché 300 s.

ParámetroValoresPor defectoSignificado
promoter_id / venue_identero positivoningunoExactamente uno, que nombre una página que el crew gestiona
{
  "data": [
    { "id": 9, "name": "Redbull", "logo_url": "https://assets.sway.events/partners/9/logo.webp", "website_url": "https://www.redbull.com", "owner": { "type": "promoter", "id": 697 } }
  ]
}

Errores: validation_error (400, no se dio exactamente uno de promoter_id / venue_id), not_found (404, la página nombrada no la gestiona el crew).

GET /v1/presskits

Los press kits de todos los artistas que gestiona el crew, con su visibilidad (público, privado, borrador) y si una contraseña los protege. Una clave publicable solo ve los públicos; una clave secreta ve los tres estados.

Permiso read:content. En caché 120 s.

{
  "data": [
    { "id": "1b2c3d4e-5f60-4a1b-9c2d-3e4f5a6b7c8d", "artist_id": 245, "title": "KROMATIEK 2026 EPK", "status": "public", "protected": false, "is_default": true, "updated_at": "2026-09-01T10:00:00.000Z", "url": "https://www.sway.events/artist/245/presskit/1b2c3d4e-5f60-4a1b-9c2d-3e4f5a6b7c8d" }
  ]
}

Sin errores propios de este endpoint.

GET /v1/presskits/{id}

Un press kit exactamente como lo muestra su propia página: el artista, las secciones en el orden elegido, sus textos y archivos. Un kit protegido pide su contraseña en la cabecera Presskit-Password, a quien llame, incluido el crew que lo gestiona.

Permiso read:content. Nunca en caché (la respuesta depende de una cabecera que la caché no ve).

curl https://www.sway.events/api/v1/presskits/1b2c3d4e-5f60-4a1b-9c2d-3e4f5a6b7c8d \
  -H "Authorization: Bearer sway_sk_YOUR_KEY" \
  -H "Presskit-Password: secret"
{
  "id": "1b2c3d4e-5f60-4a1b-9c2d-3e4f5a6b7c8d",
  "artist_id": 245,
  "title": "KROMATIEK 2026 EPK",
  "status": "public",
  "protected": false,
  "is_default": true,
  "updated_at": "2026-09-01T10:00:00.000Z",
  "url": "https://www.sway.events/artist/245/presskit/1b2c3d4e-5f60-4a1b-9c2d-3e4f5a6b7c8d",
  "artist": { "id": 245, "name": "KROMATIEK", "image_url": "https://assets.sway.events/artists/245/cover.webp", "description": "Ghent-based techno artist.", "genres": ["Techno"], "links": null, "is_verified": true, "managed": true, "contact_emails": [] },
  "sections": ["hero", "music", "bio", "photos", "contact"],
  "style": { "accent_color": "#111111", "hero_image_url": null },
  "bio_version": "short",
  "bio_overrides": {},
  "press_quotes": [],
  "files": [{ "id": "a1b2...", "type": "photo", "url": "https://assets.sway.events/presskits/.../photo.webp", "filename": "kromatiek-1.webp", "size_bytes": 240811 }]
}

Un kit privado solo se abre con la clave secreta del crew que lo gestiona; un borrador nunca es accesible. Errores: not_found (404, id mal formado, kit no encontrado, artista no gestionado, el kit es un borrador, o un kit privado llamado con el tipo de clave equivocado), password_required (401, falta la contraseña o es incorrecta), rate_limited (429, 10 intentos de contraseña por minuto como máximo por clave y kit, y por dirección de visitante cuando se llama con una clave publicable).

GET /v1/forms

Los formularios del crew y de las páginas que gestiona, sin los borradores, cada uno indicando si acepta respuestas ahora mismo.

Permiso read:content. En caché 120 s.

{
  "data": [
    {
      "slug": "backstage-crew", "title": "Join the backstage crew", "description": null,
      "availability": "open", "opens_at": null, "closes_at": null,
      "owner": { "type": "promoter", "id": 697 },
      "url": "https://www.sway.events/f/backstage-crew"
    }
  ]
}

availability es uno de draft, scheduled, open, full, closed, empty (un formulario publicado sin ninguna pregunta). Errores: ninguno más allá de los de autenticación.

GET /v1/forms/{slug}

Las preguntas, el diseño y la disponibilidad de un formulario, para representarlo en la web del socio, exactamente como lo haría su propia página pública.

Permiso read:content. En caché 60 s.

{
  "id": "9f2c1a7e-4d3b-4e21-8c7a-1b2c3d4e5f60",
  "slug": "backstage-crew",
  "title": "Join the backstage crew",
  "description": null,
  "availability": "open",
  "opens_at": null,
  "closes_at": null,
  "captures_contact": true,
  "definition_version": 3,
  "settings": { "show_progress": true, "submit_label": null, "consent_text": null },
  "steps": [{ "id": "s1", "fields": [{ "id": "email", "type": "email", "label": "Email", "required": true }] }],
  "design": {},
  "url": "https://www.sway.events/f/backstage-crew"
}

steps y design son la definición del formulario tal como la guarda el constructor de formularios, pasada tal cual: las respuestas que envías a POST /v1/forms/{slug}/submit se indexan por los ids de pregunta dentro de steps. captures_contact indica si una dirección dada en el formulario entra en los contactos del organizador.

Errores: not_found (404, forma de slug incorrecta, formulario en borrador, o un formulario que no pertenece al crew ni a una de sus páginas).


Exportación

Ninguno de estos endpoints acepta una clave publicable; cada uno necesita read:export más el permiso de lectura correspondiente. Nunca en caché (no-store), y cada uno está limitado a una exportación por recurso y clave cada cinco minutos, pasado lo cual la respuesta es rate_limited (429). Cada archivo tiene un tope de 10.000 líneas e indica si se cortó con una cabecera X-Export-Truncated: true|false.

GET /v1/export/events

GET /v1/export/artists

GET /v1/export/promoters

GET /v1/export/venues

El catálogo publicado del crew, un recurso cada vez, como JSON delimitado por saltos de línea (application/x-ndjson): un objeto JSON por línea, sin sobre, sin paginación. Cada línea es exactamente el DTO que entrega el endpoint de lista correspondiente (GET /v1/events, GET /v1/artists, GET /v1/promoters, GET /v1/venues), así que una exportación nunca muestra más de lo que ya muestra la API.

Permisos read:export + read:events / read:artists / read:promoters / read:venues respectivamente.

{"id":4521,"title":"Overload","starts_at":"2026-09-12T20:00:00.000Z", ..., "venue":{"id":88,"name":"Jungle Bar"}}
{"id":4522,"title":"Warehouse Night","starts_at":"2026-09-19T21:00:00.000Z", ..., "venue":null}

La respuesta se descarga como sway-events.ndjson, sway-artists.ndjson, sway-promoters.ndjson o sway-venues.ndjson (Content-Disposition: attachment). Errores: rate_limited (429, ya se ejecutó una exportación de este recurso con esta clave en los últimos cinco minutos).


Solicitudes de booking

POST /v1/booking-requests

Envía una solicitud de booking desde la web de un socio al pipeline de booking del crew y avisa al crew. Todo artista solicitado debe ser un artista gestionado del roster.

Permiso write:bookings (se activa aparte). Claves publicables: no. Nunca en caché. Acepta Idempotency-Key. Con límite más estricto que las lecturas: 10 peticiones por minuto y visitante (client_ip), y 300 al día por crew.

CampoTipoObligatorioNotas
artist_idsnumber[]síNo vacío, 50 ids como máximo, cada uno un artista gestionado del roster
contact_emailstringsíEmail válido, 320 caracteres como máximo
contact_namestringno200 caracteres como máximo
contact_phonestringno200 caracteres como máximo
event_namestringno200 caracteres como máximo
event_datestringnoFecha ISO, YYYY-MM-DD
venuestringno200 caracteres como máximo
citystringno200 caracteres como máximo
fee_offerstringnoTexto libre, 200 caracteres como máximo
capacitystringnoTexto libre, 200 caracteres como máximo
ticket_pricestringnoTexto libre, 200 caracteres como máximo
organizationstringno200 caracteres como máximo
{
  "artist_ids": [245],
  "contact_email": "[email protected]",
  "contact_name": "Jane Doe",
  "event_name": "Warehouse Night",
  "event_date": "2026-11-21",
  "venue": "Fuse",
  "city": "Brussels",
  "fee_offer": "800 EUR"
}

Respuesta 201 Created, devuelta directamente:

{ "id": "9f2c1a7e-4d3b-4e21-8c7a-1b2c3d4e5f60", "status": "new", "created_at": "2026-07-03T14:00:00.000Z" }

Errores: validation_error (422, forma del cuerpo incorrecta, o artist_ids nombra un id fuera del roster, listado bajo errors[].invalid_ids), rate_limited (429, límite por visitante o por crew y día).


Audiencia

Ninguno de estos endpoints acepta una clave publicable.

POST /v1/newsletter-signup

Añade un email a la newsletter de un promotor gestionado como contacto del CRM, con un registro de consentimiento RGPD.

Permiso write:newsletter (se activa aparte). Nunca en caché. Acepta Idempotency-Key. Con límite de 10 peticiones por minuto y visitante (client_ip), y 500 altas al día por promotor.

CampoTipoObligatorioNotas
emailstringsíEmail válido, 320 caracteres como máximo
promoter_idnumbersíUn promotor que gestiona el crew
first_name / last_namestringno200 caracteres como máximo
localestringno10 caracteres como máximo
countrystringno2 caracteres como máximo
{ "email": "[email protected]", "promoter_id": 697, "first_name": "Jane" }

Respuesta 201 Created: { "status": "subscribed" }.

Errores: validation_error (422, cuerpo incorrecto, o promoter_id no gestionado por el crew), rate_limited (429).

POST /v1/newsletter-unsubscribe

Quita un email de la newsletter de un promotor gestionado y de su proveedor de lista de correo: el mismo estado de CRM que respeta cada sincronización posterior.

Permiso write:newsletter (se activa aparte). Nunca en caché. Acepta Idempotency-Key. Con límite de 10 peticiones por minuto y visitante (client_ip), y 500 al día por promotor.

CampoTipoObligatorioNotas
emailstringsíEmail válido
promoter_idnumbersíUn promotor que gestiona el crew

Respuesta 200 OK: siempre { "status": "unsubscribed" }, estuviera o no la dirección en la lista: el endpoint nunca debe decirle a quien llama quién está suscrito.

Errores: validation_error (422, cuerpo incorrecto, o promoter_id no gestionado), rate_limited (429).

POST /v1/forms/{slug}/submit

Envía las respuestas de un formulario que la web del socio representó a partir de GET /v1/forms/{slug}: validadas contra la definición guardada, archivadas en el CRM del propietario cuando el formulario captura un contacto, con api:form:{slug} registrado como origen del consentimiento.

Permiso write:forms (se activa aparte). Nunca en caché. Acepta Idempotency-Key. Con límite de 60 envíos por minuto y clave y formulario, 5 por minuto y dirección de visitante (cuando se reenvía client_ip), y 5.000 al día por formulario.

CampoTipoObligatorioNotas
answersobjectsíIndexado por id de campo, tal como los nombra la propia definición del formulario
localestringnoEtiqueta de idioma de dos letras, opcionalmente con una región
honeypotstringnoUn campo oculto del propio formulario del socio: cualquier texto en él y la respuesta se descarta en silencio
client_ipstringnoLa dirección del visitante, usada solo para contar sus envíos
{ "answers": { "email": "[email protected]", "s1_q1": "Yes" } }

Respuesta 201 Created: { "status": "received", "redirect_url": null } (un honeypot relleno responde exactamente lo mismo, pero no guarda nada).

Errores: not_found (404, slug incorrecto, formulario en borrador, o un formulario que no pertenece al crew), validation_error (422, cuerpo mal formado o respuestas que fallan la propia validación del formulario, bajo errors[].param como answers.<field>), conflict (409, not_taking_answers cuando el formulario no está abierto ahora mismo, already_answered cuando el formulario solo permite una respuesta por dirección y esta ya respondió), rate_limited (429).


Checkout

Ninguno de estos endpoints acepta una clave publicable.

POST /v1/checkout-sessions

Crea una sesión de checkout alojado de Stripe para una tienda pública y devuelve su url. El comprador es redirigido ahí, y después vuelve a la propia página /success o /cancel de Sway, nunca al sitio de quien llama; el cumplimiento (pedido, entradas, email de confirmación) es enteramente de Sway.

Permiso write:checkout (se activa aparte); un carrito cuyo total es exactamente cero necesita además write:checkout:free. Nunca en caché. No acepta Idempotency-Key: este endpoint mantiene su propia protección en su lugar (idempotency_key en el cuerpo, más abajo). Con límite de 60 peticiones por minuto y comprador (client_ip), 600 por minuto y clave, 5.000 checkouts al día por crew, y 30 checkouts de pago (o 10 gratuitos) al día por email de comprador.

Todo lo que decide sobre el dinero y el stock se resuelve en el servidor y quien llama nunca puede aportarlo ni sustituirlo: el precio, la moneda, la cuenta de Stripe del beneficiario, la tarifa de servicio y la reserva de stock. El crew de la clave debe gestionar el promotor que recibe los fondos, si no validation_error.

CampoTipoObligatorioNotas
event_idnumbersíDebe estar publicado y en el grafo del crew
line_itemsarraysíDe 1 a 20 entradas { "tier_id": string, "quantity": number }; tier_id es un id de tipo de entrada de GET /v1/events/{id}/ticket-tiers, quantity de 1 a 50
buyer_emailstringsíEmail válido, 320 caracteres como máximo: adónde se entregan las entradas
idempotency_keystringnoUn UUID por clic de compra; un reintento con el mismo valor devuelve la misma sesión
discount_idstringnoUn id de cupón del evento, de POST /v1/coupons/validate
access_codestringnoUn código de preventa o de acceso que desbloquea un tipo oculto
referencestringnoTexto libre, 64 caracteres como máximo de [A-Za-z0-9._:-], repetido solo en analítica
localestringnoEl idioma del comprador (en, fr, nl...), usado para nombrar la línea de comisión de servicio en la página de pago de Stripe. Por defecto el Accept-Language de la petición, luego inglés
marketing_consentbooleannoSe ignora en esta vía; un consentimiento afirmado por una clave de API nunca es de fiar para inscribir en el CRM
consentobjectnoBooleanos { analytics, marketing } más version/at, reenviados solo al registro de atribución publicitaria
client_ip / client_user_agent / event_source_urlstringnoSeñales del lado del comprador para el emparejamiento de atribución publicitaria, y client_ip para contar los checkouts del comprador. Nunca usadas para otra cosa

Campos rechazados con validation_error si se envían, porque cada uno se resuelve en el servidor: amount / price / unit_amount, currency, cualquier beneficiario o stripe_account, application_fee_amount / fee, success_url / cancel_url / return_url, coupon / coupon_code / stripe_coupon_id, payment_method_types, user_id.

{
  "event_id": 4521,
  "line_items": [{ "tier_id": "8f14e45f-ceea-467a-9575-b2c45e4f2a11", "quantity": 2 }],
  "buyer_email": "[email protected]",
  "idempotency_key": "b1e2c3d4-5f60-4a1b-9c2d-3e4f5a6b7c8d"
}

Respuesta 200 OK, devuelta directamente:

{ "url": "https://checkout.stripe.com/c/pay/cs_live_...", "checkout_id": "chk_9f2c1a7e..." }

checkout_id solo acompaña a una sesión de pago; un pedido gratuito (de cortesía) devuelve solo { "url": ... }, ya que no hay ninguna sesión de Stripe que seguir después.

Un checkout de pago retiene sus entradas, y un uso de su cupón y de su código de acceso, durante los 30 minutos de la sesión. Lo que el comprador no paga vuelve cuando la sesión expira; hasta entonces, un código cuyo último uso está retenido responde validation_error.

Pedidos gratuitos (0 euros). Un carrito cuyo total es cero, por un cupón del 100 % de descuento o un tipo realmente gratuito, es una capacidad aparte y de más confianza: la clave necesita además write:checkout:free. Las entradas gratuitas y de pago no se pueden combinar en un mismo pedido.

Errores: missing_scope (403, falta write:checkout, o un pedido gratuito sin write:checkout:free), not_found (404, evento no publicado o fuera del grafo), validation_error (422, cuerpo incorrecto, tipo desconocido o no disponible, cantidad por encima de max_per_order, líneas gratuitas y de pago mezcladas, o el beneficiario no gestionado por el crew), conflict (409, la cuenta de pagos del promotor no está lista), rate_limited (429, mira Retry-After), upstream_error (502, Stripe no pudo crear la sesión).

GET /v1/checkout-sessions/{id}

En qué punto está un checkout que abrió este crew, referenciado por el checkout_id que devolvió su creación, nunca el id de sesión de Stripe: abierto (con su enlace de pago), completo o caducado.

Permiso write:checkout. Nunca en caché (no-store).

{
  "checkout_id": "chk_9f2c1a7e...",
  "status": "open",
  "payment_status": "unpaid",
  "amount_total": 4552,
  "currency": "eur",
  "expires_at": "2026-09-26T13:00:00.000Z",
  "reference": null,
  "url": "https://checkout.stripe.com/c/pay/cs_live_..."
}

url solo está presente mientras status es open. Errores: not_found (404, identificador mal formado, o un checkout sellado para otro crew).

POST /v1/checkout-sessions/{id}/expire

Termina un checkout abierto de este crew y devuelve sus entradas al stock de inmediato, en vez de esperar al temporizador propio de Stripe. Idempotente: volver a llamarlo sobre un checkout ya caducado simplemente responde con su estado actual.

Permiso write:checkout. Nunca en caché. Acepta Idempotency-Key.

Misma forma de respuesta que GET /v1/checkout-sessions/{id}. Errores: not_found (404, igual que arriba), conflict (409, el checkout ya estaba pagado: un reembolso pasa por el organizador en su lugar).

POST /v1/coupons/validate

Comprueba un código de promoción antes del checkout: el discount_id a pasar, sus condiciones, y los totales del carrito cuando se envía uno, o por qué no se aplica. Responde 200 en cualquier caso: un código rechazado es una respuesta, no un error.

Permiso write:checkout. Nunca en caché. No acepta Idempotency-Key (writeClass: check): se ejecuta cada vez, hasta 300 comprobaciones por minuto y clave, encima de los límites de intentos de más abajo.

CampoTipoObligatorioNotas
event_idnumbersíDebe ser un evento cuyo beneficiario gestione el crew
codestringsíDe 1 a 64 caracteres imprimibles
line_itemsarraynoEl carrito del checkout, misma forma que POST /v1/checkout-sessions
client_ipstringnoLa dirección del comprador, usada solo para contar sus fallos
{ "event_id": 4521, "code": "EARLY10", "line_items": [{ "tier_id": "8f14e45f-ceea-467a-9575-b2c45e4f2a11", "quantity": 2 }] }
{
  "valid": true, "code": "EARLY10", "discount_id": "f4c8b1a2-...",
  "percentage": 10, "amount": null, "products_remaining": 4,
  "totals": { "currency": "EUR", "subtotal": 5000, "discount": 500, "total": 4500 }
}

Un código que pertenece a otro evento, a otro organizador, o que está desactivado, responden todos igual: { "valid": false, "code": "...", "reason": "not_found" }, sin distinción. Otros valores de reason: min_order (con min_order_amount), product_limit (con products_remaining), expired, usage_limit, not_open_here, not_applicable. El id de cupón de Stripe nunca sale de ahí.

Errores: validation_error (422, cuerpo incorrecto), not_found (404, evento no publicado o fuera del grafo), validation_error (422, beneficiario no gestionado por el crew), rate_limited (429, el techo de comprobaciones por minuto, o el límite de intentos de más abajo).

POST /v1/access-codes/validate

Comprueba un código de preventa o de acceso: los tipos de entrada ocultos que desbloquea (nunca listados por GET /v1/events/{id}/ticket-tiers), si el organizador quiere que se muestren solo esos tipos, y el tope por pedido propio del código, o por qué no funciona.

Permiso write:checkout. Nunca en caché. No acepta Idempotency-Key (writeClass: check), mismo techo de 300 comprobaciones por minuto que los cupones.

CampoTipoObligatorioNotas
event_idnumbersíDebe ser un evento cuyo beneficiario gestione el crew
codestringsíDe 1 a 64 caracteres imprimibles
client_ipstringnoLa dirección del comprador, usada solo para contar sus fallos
{
  "valid": true, "code": "VIP", "name": "Members", "exclusive": true, "max_per_order": 2,
  "tiers": [{ "id": "8f14e45f-ceea-467a-9575-b2c45e4f2a11", "name": "Presale", "description": null, "price": 25.0, "currency": "EUR", "status": "on_sale", "sale_start": null, "sale_end": null, "max_per_order": 4, "checkout_url": "https://www.sway.events/event/4521?ref=api&code=VIP" }]
}

exclusive: true significa que el organizador quiere que, una vez introducido el código, solo se muestren estos tipos desbloqueados. Los ids internos propios del código nunca salen de ahí. Otros valores de reason de rechazo: not_found, inactive, not_started, ended, usage_limit.

Límites de intentos (los dos endpoints de arriba). Más allá del techo general de comprobaciones por minuto, un código desconocido se cuenta por clave y evento (300 fallos cada 10 minutos) y, cuando quien llama reenvía client_ip, también por dirección del comprador (10 fallos cada 15 minutos): pasado cualquiera de los dos límites, la respuesta es rate_limited (429) antes incluso de buscar el código.

Errores: los mismos que POST /v1/coupons/validate.


Tienda (shop)

Las tres lecturas aceptan una clave publicable; el presupuesto y el checkout, no.

El merch que venden las páginas que gestiona tu crew, para una tienda propia: el catálogo de una página, un producto, lo que vende un evento, el precio de un carrito y un checkout de Stripe en la cuenta de la página que vende. El comprador paga en Stripe y vuelve a las páginas de Sway; Sway registra el pedido, escribe al comprador y al vendedor, y el vendedor gestiona el envío, los desistimientos y los reembolsos desde la pantalla Shop de la página.

Una página vende en cuanto su tienda está abierta; antes, su catálogo se lee vacío. Los precios incluyen IVA y van en céntimos de euro. Ninguna ruta da un stock: una variante está available o no.

GET /v1/shop/products

El merch publicado de una página del crew, con la identidad del vendedor que el comprador debe ver antes de pagar. Las exclusivas de evento no están: léelas en el evento.

Permiso read:shop. Claves publicables: sí. En caché 30 s.

ParámetroTipoObligatorioNotas
seller_typestringsípromoter, artist o venue
seller_idnumbersíUna página que el crew gestiona
{
  "seller": {
    "type": "promoter", "id": 697, "name": "Insomnia Nights", "image_url": null,
    "legal_name": "Insomnia Nights SRL", "address": "Rue de la Loi 1, 1000 Brussels, BE",
    "company_number": "BE0123456789", "contact_email": "[email protected]",
    "vat_regime": "registered", "return_shipping_paid_by": "buyer"
  },
  "installments": { "provider": "stripe", "publishable_key": "pk_live_...", "stripe_account": "acct_1Abc...", "country": "BE", "methods": ["klarna"] },
  "data": [
    {
      "id": "3f2c5e1a-7b4d-4c1e-9a2b-5d6e7f8a9b0c", "slug": "tour-hoodie-2026", "title": "Tour Hoodie 2026",
      "description": "Heavyweight cotton.",
      "images": [{ "url": "https://assets.sway.events/shop/hoodie.webp", "alt": null, "width": 1200, "height": 1200 }],
      "options": ["Size"],
      "variants": [{ "id": "9a1b7c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d", "options": ["M"], "price_cents": 4500, "available": true, "max_per_order": 20 }],
      "from_price_cents": 4500, "currency": "EUR", "available": true,
      "fulfilment_modes": ["delivery", "pickup_event"], "country_rule": "all", "countries": [], "exclusive": false,
      "manufacturer": { "name": "Insomnia Nights SRL", "address": "Rue de la Loi 1, 1000 Brussels", "email": "[email protected]" },
      "eu_responsible": null, "safety_info": "Wash at 30 degrees.", "preparation_days": 2, "preorder": null,
      "seller": { "type": "promoter", "id": 697 }
    }
  ]
}

Una lista pequeña: envuelta en { "data": [...] }, sin pagination. Una página que el crew no gestiona, o cuya tienda no está abierta, responde { "seller": null, "data": [] }. fulfilment_modes lista delivery, pickup_event y pickup_location; country_rule es all, only o except, aplicado a countries. Errores: validation_error (422, seller_type o seller_id ausente o inválido).

preorder es null, o { "status": "open", "ships_on": "2026-11-15", "ends_on": null } para un producto vendido antes de estar listo: se envía a partir de ships_on y acepta reservas hasta ends_on cuando se fija. status es closed cuando la reserva terminó antes del día de envío; sus variantes indican entonces available: false. El día del envío, el producto se vende como cualquier otro y preorder vuelve a null.

installments es null, o la oferta de pago a plazos de Stripe en la cuenta de la página que vende. Cuando está presente, muestra el Payment Method Messaging Element de Stripe con publishable_key y stripe_account (Stripe.js), nunca un texto propio: la publicidad del crédito a plazos está regulada. methods lista lo que ofrece el Checkout (klarna); country es el del vendedor, un valor por defecto hasta que el comprador elige país de envío. El producto y la tienda de un evento llevan el mismo campo.

GET /v1/shop/products/{id}

Un producto para su página, devuelto directamente, con seller como identidad completa del vendedor. Con event_id, el producto tal como lo vende ese evento, exclusivas incluidas.

Permiso read:shop. Claves publicables: sí. En caché 30 s.

ParámetroTipoObligatorioNotas
event_idnumbernoUn evento del grafo del crew con la tienda abierta

Errores: not_found (404, producto desconocido o sin publicar, una página que el crew no gestiona, una exclusiva pedida sin su evento, o un evento con la tienda cerrada).

GET /v1/events/{id}/shop

Lo que vende un evento del grafo del crew: los productos de su promotor cobrador, todos o una selección, exclusivas incluidas, mientras la tienda del evento está abierta; y si el comprador aún puede elegir la recogida en el evento.

Permiso read:shop. Claves publicables: sí. En caché 30 s.

{
  "event_id": 4521,
  "seller": { "type": "promoter", "id": 697, "name": "Insomnia Nights", "legal_name": "Insomnia Nights SRL" },
  "pickup_at_event": { "open": true, "cutoff_at": "2026-10-10T20:00:00.000Z", "instructions": "Merch stand, left of the bar, from doors" },
  "data": [{ "id": "3f2c5e1a-7b4d-4c1e-9a2b-5d6e7f8a9b0c", "title": "Tour Hoodie 2026", "exclusive": false }]
}

seller lleva la identidad completa del vendedor, como en la lista de arriba. La lista está vacía, con seller y pickup_at_event a null, mientras la tienda del evento está cerrada, o cuando el promotor cobrador no es una página del crew, ya que el checkout no podría vender por él. Errores: not_found (404, evento sin publicar o fuera del grafo).

POST /v1/shop/quote

Calcula un carrito antes del checkout: las líneas tras el cupón, las opciones de envío para un modo y un país, los modos y países que el carrito admite, y el total. No se reserva nada.

Permiso read:shop. Claves publicables: no. Nunca en caché. Limitado a 120 solicitudes por minuto y comprador (client_ip).

CampoTipoObligatorioNotas
linesarraysí1 a 20 entradas { "variant_id": string, "quantity": number }, cantidad de 1 a 20
modestringsídelivery, pickup_event o pickup_location
event_idnumbernoEl evento del que viene el carrito: desbloquea sus exclusivas y la recogida en el evento
countrystringpara deliveryCódigo de dos letras del país de entrega
coupon_codestringnoUn código de la página que vende
method_keystringnoLa opción de envío elegida, de shipping_options
client_ipstringnoLa dirección del comprador, para contar sus solicitudes
{
  "currency": "EUR",
  "lines": [{ "variant_id": "9a1b7c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d", "name": "Tour Hoodie 2026 · M", "quantity": 2, "unit_cents": 4500, "discount_cents": 900 }],
  "subtotal_cents": 9000,
  "discount_cents": 900,
  "shipping_options": [
    { "key": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e", "kind": "delivery", "name": "bpost, 2 to 4 days", "carrier": "bpost", "amount_cents": 590, "min_days": 2, "max_days": 4, "pickup_address": null, "instructions": null }
  ],
  "selected_option": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
  "shipping_cents": 590,
  "total_cents": 8690,
  "available_modes": ["delivery", "pickup_event"],
  "deliverable_countries": ["BE", "FR", "NL"],
  "coupon": { "code": "TOUR10", "discount_cents": 900 },
  "ships_on": null
}

ships_on es el día de envío del pedido cuando el carrito contiene una reserva, el más tardío de ellos; el comprador lo ve antes de pagar, también en la página de Stripe. Es null para un carrito en stock. not_ready_for_event rechaza la recogida en el evento de un carrito que se envía después; preorder_closed lleva el variant_id cuya reserva terminó.

Errores: validation_error (422) con una reason: invalid_cart, invalid_mode, unavailable, out_of_stock, max_per_order, mixed_sellers, country_required, country_not_allowed, no_method, mode_not_allowed, pickup_closed, preorder_closed, not_ready_for_event, o una razón que empieza por coupon_. max_per_order lleva variant_id y max; un rechazo de envío lleva available_modes y deliverable_countries. Luego conflict (409, razón not_ready: la página aún no puede vender), not_found (404, un evento fuera del grafo) y rate_limited (429).

POST /v1/shop/cart-check

Indica qué líneas del carrito que guarda un comprador apuntan a un producto que salió de la tienda: despublicado, archivado o eliminado, o una variante desactivada o quitada. Quita esas líneas del carrito. Una variante agotada, una página que aún no puede vender o un evento con la tienda cerrada conserva su línea: eso puede cambiar, y el presupuesto lo explica. Una línea de una página que tu crew no gestiona también se devuelve.

Permiso read:shop. Claves publicables: no. Nunca en caché. Limitado a 120 solicitudes por minuto y comprador (client_ip).

CampoTipoObligatorioNotas
linesarraysí1 a 20 entradas { "variant_id": string, "product_id": string }
client_ipstringnoLa dirección del comprador, para contar sus solicitudes
{ "gone": ["9a1b7c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d"] }

Comprueba el carrito cuando el comprador vuelve al sitio y cuando se abre el carrito.

Errores: validation_error (422, una línea ausente o mal formada) y rate_limited (429).

POST /v1/shop/checkout-sessions

Abre un checkout alojado de Stripe para merch de una página que el crew gestiona, en la cuenta de Stripe de esa página, y devuelve su url. El comprador paga allí y llega a la página de agradecimiento de Sway y a la de su pedido, o a /cancel, nunca al sitio del llamante. Sway registra el pedido, escribe al comprador y al vendedor, y cobra su comisión al vendedor.

Permiso write:shop:checkout (opcional). Claves publicables: no. Nunca en caché. No acepta Idempotency-Key: envía idempotency_key en el cuerpo. Limitado a 60 solicitudes por minuto y comprador (client_ip), 5000 checkouts al día por crew y 30 al día por correo de comprador.

El cuerpo es el del presupuesto, más:

CampoTipoObligatorioNotas
buyer_emailstringsíAdónde van la confirmación y el enlace del pedido
idempotency_keystringnoUn UUID por clic en Pagar; un reintento con el mismo valor devuelve la misma sesión
localestringnoEl idioma del comprador para Stripe y los correos: en, fr, de, es, it, nl o uk
referencestringnoLibre, como máximo 64 caracteres de [A-Za-z0-9._:-], guardada en el pedido

Campos rechazados con validation_error si se envían, porque cada uno se resuelve en el servidor: amount, price, unit_amount, unit_cents, price_cents, total_cents, shipping_cents, currency, seller_type, seller_id, payee, stripe_account, application_fee_amount, fee, commission, success_url, cancel_url, return_url, discount_id, stripe_coupon_id, user_id.

{
  "lines": [{ "variant_id": "9a1b7c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d", "quantity": 2 }],
  "mode": "delivery",
  "country": "BE",
  "coupon_code": "TOUR10",
  "buyer_email": "[email protected]",
  "idempotency_key": "b1e2c3d4-5f60-4a1b-9c2d-3e4f5a6b7c8d"
}

Respuesta 200 OK: { "url": "https://checkout.stripe.com/c/pay/cs_live_..." }

El stock queda retenido durante los 30 minutos de la sesión; lo que el comprador no paga vuelve cuando la sesión caduca. Un consentimiento de marketing enviado por el llamante se ignora: una clave de API nunca inscribe a un comprador en un CRM.

Errores: los del presupuesto, más validation_error para un buyer_email o una reference inválidos, o para productos de una página que el crew no gestiona; conflict (409, not_ready, o el stock cambió bajo el carrito); rate_limited (429, mira Retry-After); upstream_error (502, Stripe no pudo crear la sesión).


Embajadores

El programa de recomendación del promotor, representado en la propia web del socio en vez de en sway.events. Ninguno de estos endpoints acepta una clave publicable: ejecútalos desde un servidor, nunca desde un navegador. Hay tres credenciales en juego: la clave de API sway_sk_... (qué crew llama), una sesión de miembro amb_st_... que viaja en la cabecera X-Ambassador-Session (qué embajador tiene la sesión iniciada, creada al registrarse o al acceder, 8 horas), y la cookie propia de origen del socio que asocia un visitante a esa sesión.

El registro y el acceso son formularios propios del socio; Sway no representa ninguna página aquí y no ve ningún widget, así que el socio pone su propia protección antibots (Turnstile, hCaptcha, un honeypot). La protección propia de Sway es estructural: el alta está desactivada salvo que el promotor la active, gasta una invitación de tienda dedicada con su propia cuota, y cada endpoint de abajo tiene un tope por clave, por visitante y, donde importa, por dirección de correo.

GET /v1/ambassador-program

Todo lo que necesita una página de "hazte embajador": el nombre y la descripción del programa, el descuento que ofrece, si ahora mismo acepta gente, y las palabras propias del promotor para XP, niveles, misiones e insignias. Solo datos públicos, ningún miembro ni recuento de miembros.

Permiso read:ambassadors. En caché 120 s.

ParámetroValoresPor defectoSignificado
owner_typepromoter, venueobligatorioDe quién es el programa
owner_identero positivoobligatorioEl id de la página
{
  "ok": true,
  "program": { "name": "Insomnia Ambassadors", "description": "Bring your friends, earn rewards." },
  "owner": { "type": "promoter", "id": 697 },
  "discount": { "percentage": 15 },
  "signup": { "open": true, "seats_left": 42 }
}

La respuesta también repite name, description, signup_open y gamification_enabled en el primer nivel, para los sitios construidos sobre la primera versión del boilerplate de Insomnia. Lee los campos anidados: son los que describe esta página.

Un propietario fuera del grafo del crew responde el mismo 404 que uno sin programa: este endpoint nunca debe decir cuáles de los promotores que la clave no gestiona tienen uno.

Errores: validation_error (422, owner_type / owner_id ausente o mal formado: este endpoint es anterior a la regla de 400 para parámetros de consulta y conserva su respuesta), not_found (404).

GET /v1/ambassador-leaderboard

La clasificación del programa, solo cuando el promotor la publica: puesto, nombre de pila e inicial del apellido, puntuación. Una clasificación por ingresos da el orden, nunca las cantidades.

Permiso read:ambassadors. En caché 60 s.

ParámetroValoresPor defectoSignificado
owner_typepromoter, venueobligatorioDe quién es el programa
owner_identero positivoobligatorioEl id de la página
limitentero, 1 a 10025Cuántos puestos
{
  "metric": "tickets",
  "data": [
    { "rank": 1, "name": "Melanie P.", "score": 42, "tier": { "name": "Gold", "color": "#f5c542" } },
    { "rank": 2, "name": "Tom D.", "score": 37, "tier": null }
  ]
}

metric es tickets, xp o revenue. Con revenue, score es null: el orden muestra quién vendió más, nunca cuánto. tier solo aparece mientras los niveles del programa están activados.

Errores: validation_error (400, owner_type / owner_id ausente o mal formado), not_found (404, sin programa, o el programa no publica su clasificación).

GET /v1/ambassador-rewards

Las recompensas en las que los miembros gastan sus créditos: coste, cuánto queda, cuándo, y el nivel que necesitan. Vacío mientras los créditos están desactivados.

Permiso read:ambassadors. En caché 60 s.

ParámetroValoresPor defectoSignificado
owner_typepromoter, venueobligatorioDe quién es el programa
owner_identero positivoobligatorioEl id de la página
{
  "credits_enabled": true,
  "label_singular": "credit",
  "label_plural": "credits",
  "data": [
    {
      "id": "7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
      "name": "Backstage tour",
      "description": "Meet the crew before doors open.",
      "image_url": null,
      "cost": 50,
      "stock_left": 8,
      "max_per_member": 1,
      "available_from": null,
      "available_until": "2026-12-31T23:00:00+00:00",
      "available": true,
      "event": { "id": 4521, "title": "Insomnia Night: Autumn Opening" },
      "min_tier": { "id": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091", "name": "Gold", "min_xp": 500 },
      "claim_prompt": "Which night suits you?"
    }
  ]
}

stock_left es null cuando la recompensa no tiene límite de stock. available es false fuera de sus fechas o en cuanto se agota. claim_prompt es la pregunta que responde el miembro al canjearla (el answer de POST /v1/ambassador-claim).

Errores: validation_error (400, owner_type / owner_id), not_found (404, sin programa).

GET /v1/ambassador-quests

Las misiones que los miembros pueden emprender ahora: objetivo, fechas y recompensas. El progreso propio de un miembro con la sesión iniciada está en GET /v1/ambassador-me, nunca aquí.

Permiso read:ambassadors. En caché 60 s.

ParámetroValoresPor defectoSignificado
owner_typepromoter, venueobligatorioDe quién es el programa
owner_identero positivoobligatorioEl id de la página
{
  "enabled": true,
  "quest_noun": "quest",
  "xp_label": "XP",
  "badge_noun": "badge",
  "data": [
    {
      "id": "3d4e5f60-7182-4a93-b4c5-d6e7f8091a2b",
      "name": "Bring five friends",
      "description": null,
      "image_url": null,
      "metric": "tickets",
      "scope": "event",
      "threshold": 5,
      "event": { "id": 4521, "title": "Insomnia Night: Autumn Opening" },
      "starts_at": null,
      "ends_at": "2026-10-03T20:00:00+00:00",
      "xp_reward": 100,
      "credit_reward": 20,
      "badge": { "id": "4e5f6071-8293-4a4b-85c6-e7f8091a2b3c", "name": "Recruiter", "image_url": null, "icon": "users", "rarity": "silver" }
    }
  ]
}

metric es aquello de lo que el miembro tiene que alcanzar threshold: orders, tickets, revenue_cents, credits_earned, reward_claimed, profile_complete o manual (lo marca el organizador). scope es lifetime (desde que el miembro se unió), event (en un evento, o en cualquier evento suelto cuando event es null) o window (entre starts_at y ends_at). data está vacío mientras la gamificación está desactivada.

Errores: validation_error (400, owner_type / owner_id), not_found (404, sin programa).

GET /v1/ambassadors

Los miembros del programa para las herramientas propias del promotor, el más reciente primero: nombre, email, Instagram, código, estado, ventas, créditos, XP y nivel. Nunca el teléfono, el token de acceso ni la cuenta de Sway.

Permiso read:ambassador-members (solo propietario: solo el propietario del crew puede concederlo). Nunca en caché (no-store).

ParámetroValoresPor defectoSignificado
owner_typepromoter, venueobligatorioDe quién es el programa
owner_identero positivoobligatorioEl id de la página
qtextoningunoCoincide con nombre, email, Instagram o código
statusactive, pending, suspendedningunoFiltra por estado
limitentero, 1 a 10025Tamaño de página
cursorcadena opacaningunoDe una página anterior
{
  "data": [
    {
      "id": "5f0c6a0e-2b1d-4c3e-9f8a-7b6c5d4e3f21", "first_name": "Melanie", "last_name": "Pauwels",
      "email": "[email protected]", "instagram_handle": "mel", "locale": "nl", "status": "active", "code": "MELANPAU",
      "sales": { "orders": 4, "tickets": 9, "revenue_cents": 27000 }, "credit_balance": 12, "xp": 340, "tier": "Gold",
      "joined_at": "2026-09-01T00:00:00.000Z"
    }
  ],
  "pagination": { "next_cursor": null, "has_more": false, "limit": 25 }
}

Errores: validation_error (400, owner_type / owner_id / status), invalid_cursor (400), not_found (404, sin programa para este propietario).

GET /v1/ambassadors/{id}/ledger

El historial de créditos de un miembro, el más reciente primero, con su saldo corriente: lo que sumó o restó cada venta, canje, misión o concesión manual.

Permiso read:ambassador-members. Nunca en caché.

ParámetroValoresPor defectoSignificado
limitentero, 1 a 10025Tamaño de página
cursorcadena opacaningunoDe una página anterior
{
  "member": { "id": "5f0c6a0e-2b1d-4c3e-9f8a-7b6c5d4e3f21", "first_name": "Melanie", "last_name": "Pauwels", "status": "active" },
  "credits_enabled": true,
  "balance": 12,
  "data": [{ "id": "7a1b...", "amount": 3, "reason": "ticket_sale", "note": null, "created_at": "2026-09-20T18:00:00.000Z" }],
  "pagination": { "next_cursor": null, "has_more": false, "limit": 25 }
}

El propietario que el crew debe gestionar se lee del programa propio del miembro, nunca de la petición: el miembro de otro crew responde el mismo 404 que un id desconocido. Errores: not_found (404, id mal formado o miembro fuera de los programas del crew), invalid_cursor (400).

GET /v1/ambassador-stats

Cómo le va al programa: miembros por estado y la proporción de los activos que venden, las ventas que trajeron sus códigos (de toda la vida, y entre from y to cuando se dan), y los diez mejores códigos en el mismo periodo.

Permiso read:ambassador-members. Nunca en caché.

ParámetroValoresPor defectoSignificado
owner_typepromoter, venueobligatorioDe quién es el programa
owner_identero positivoobligatorioEl id de la página
from / tofecha o fecha y hora ISO 8601ningunoVentana, 400 días de separación como máximo, to después de from
{
  "members": { "total": 12, "active": 8, "pending": 3, "suspended": 1, "selling": 3, "selling_rate": 0.375 },
  "lifetime": { "orders": 20, "tickets": 41, "revenue_cents": 90000 },
  "window": null,
  "top_codes": []
}

Errores: validation_error (400, owner_type / owner_id, o una ventana invertida o de más de 400 días), not_found (404, sin programa).

POST /v1/ambassador-register

Alguien se une al programa desde la propia web del socio. Devuelve su código y una sesión de miembro, para que la tienda pueda mostrarle su página de inmediato.

Permiso write:ambassadors (se activa aparte). Nunca en caché. Acepta Idempotency-Key. Con límite de 30 altas por minuto y visitante (client_ip, pensado para el wifi compartido de un lugar), y 300 al día por programa.

CampoTipoObligatorioNotas
owner_typestringsípromoter o venue
owner_idnumbersíEl id de la página
first_name / last_namestringsí120 caracteres como máximo
emailstringsíEmail válido; la única forma de volver a una página perdida
phonestringsíUn número de teléfono real; la clave de deduplicación del programa
localestringno8 caracteres como máximo
{ "owner_type": "promoter", "owner_id": 697, "first_name": "Jane", "last_name": "Doe", "email": "[email protected]", "phone": "+32470000000" }

Respuesta: 201 cuando el miembro es realmente nuevo, 200 cuando ya existía:

{ "created": true, "member_id": "6b1d...", "code": "MELANPAU", "status": "active", "session": { "token": "amb_st_...", "expires_at": "2026-09-01T18:00:00.000Z" } }

Un número de teléfono ya inscrito en este programa se rechaza, no se inicia sesión con él: quien escribió el número no es necesariamente su dueño.

Errores: validation_error (422, campos ausentes o no válidos, o un email desechable o bloqueado), not_found (404, propietario fuera del grafo del crew), conflict (409, signup_closed salvo que el promotor haya activado el alta pública y una invitación de tienda activa tenga cuota, o already_registered para un teléfono ya inscrito), rate_limited (429).

POST /v1/ambassador-login

Envía por email a un miembro un enlace de acceso de un solo uso que vuelve a la propia web del socio.

Permiso write:ambassadors. Nunca en caché. Acepta Idempotency-Key. Con límite de 8 por minuto y visitante (client_ip), y 5 por hora y dirección de correo para el envío en sí.

CampoTipoObligatorioNotas
owner_typestringsípromoter o venue
owner_idnumbersíEl id de la página
emailstringsíAdónde enviar el enlace
return_originstringsíUno de los orígenes registrados en esta clave
return_pathstringnoUna ruta relativa en ese origen, por defecto /ambassador

Respuesta 202, siempre el mismo cuerpo: { "status": "accepted" }. Una dirección desconocida, un miembro suspendido, un programa cerrado y un envío real son indistinguibles: un formulario de acceso que respondiera distinto para una dirección conocida sería un oráculo de pertenencia. return_origin y return_path se vuelven a validar en SQL, así que no hay forma de montar una redirección abierta.

Errores: validation_error (422, cuerpo incorrecto, o return_origin no es uno de los orígenes registrados de la clave), rate_limited (429, por visitante o por dirección de correo).

POST /v1/ambassador-login-exchange

La página de llegada del socio recibe ?amb_token=... y lo cambia, en el servidor, por una sesión de miembro.

Permiso write:ambassadors. Nunca en caché. Acepta Idempotency-Key. Con límite de 20 intentos por minuto y visitante (client_ip).

CampoTipoObligatorioNotas
tokenstringsíEl valor amb_lt_... de la query string
{ "session": { "token": "amb_st_...", "expires_at": "2026-09-01T18:00:00.000Z" }, "return_url": "https://www.example.com/ambassador" }

Un solo uso: el token se gasta con la misma actualización de base de datos que lo lee, así que dos toques en un cliente de correo que precarga enlaces no pueden tener éxito los dos.

Errores: invalid_key (401, token mal formado, desconocido, caducado o ya usado, todos idénticos), rate_limited (429).

GET /v1/ambassador-me

La página propia del miembro con la sesión iniciada: código, ventas, créditos y recompensas, más XP, nivel, misiones e insignias cuando el promotor activó la gamificación.

Permiso read:ambassadors, más una cabecera X-Ambassador-Session. Nunca en caché.

{
  "membership": {
    "member": { "id": "0b7c9d1e-2f3a-4b5c-8d6e-7f8091a2b3c4", "first_name": "Melanie", "last_name": "Paulus", "status": "active", "code": "MELANPAU" },
    "program": { "id": "5e6f7081-92a3-4b4c-95d6-e7f8091a2b3c", "name": "Insomnia Ambassadors", "description": null, "leaderboard_visible": true, "owner": { "id": 697, "type": "promoter", "name": "Insomnia", "image_url": null } },
    "owner": { "id": 697, "type": "promoter", "name": "Insomnia", "image_url": null },
    "stats": { "orders": 4, "tickets": 9, "revenue_cents": 27000 },
    "credits": { "enabled": true, "balance": 12, "label_singular": "credit", "label_plural": "credits" },
    "game": { "enabled": true, "xp_label": "XP", "lifetime_xp": 340, "tier": { "id": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091", "name": "Gold", "min_xp": 300, "color": "#f5c542" }, "next_tier": null, "badges_earned": 2, "quests_completed": 3 },
    "tiers": [],
    "quests": [],
    "badges": [],
    "xp_ledger": [],
    "rewards": [],
    "claims": [],
    "ledger": [],
    "events": [],
    "leaderboard": []
  },
  "session": { "expires_at": "2026-09-26T20:00:00+00:00" }
}

La misma página de miembro que muestra sway.events, resumida aquí: game también lleva las palabras propias del programa (tier_noun, quest_noun, badge_noun) y next_tier dice cuánto XP falta para subir (null en lo más alto). quests, badges, tiers y xp_ledger se rellenan solo mientras la gamificación está activada, rewards, claims y ledger (el historial de créditos) solo mientras los créditos están activados. events lista los eventos en los que funciona el código del miembro, con su descuento, y leaderboard la clasificación cuando el promotor la publica, con revenue_cents en cada puesto solo en una clasificación por ingresos, igual que en sway.events.

access_token (la credencial permanente del miembro para su página en sway.events) y su número de teléfono nunca aparecen aquí, sea lo que sea lo que contenga la fila por debajo.

Errores: invalid_key (401, falta la cabecera o está mal formada, o la sesión ha caducado o no es válida).

POST /v1/ambassador-profile

El miembro con la sesión iniciada corrige su propio nombre, email o idioma.

Permiso write:ambassadors, más sesión. Nunca en caché. Acepta Idempotency-Key. Con límite de 20 por minuto y visitante (client_ip).

CampoTipoObligatorioNotas
first_name / last_name / email / localestringnoCualquier subconjunto; se necesita al menos uno

El número de teléfono no se puede cambiar aquí (es la clave de deduplicación: cambiarlo sería una fusión, no una edición), ni tampoco el estado, el código, los créditos o el XP: el código no sigue a un cambio de nombre.

Errores: invalid_key (401, sesión), validation_error (422, email incorrecto, ningún campo enviado, o un email desechable o bloqueado), rate_limited (429).

POST /v1/ambassador-claim

El miembro con la sesión iniciada gasta créditos en una recompensa. Quien llama nunca indica un precio: el coste, el saldo, el stock, el límite por miembro y la barrera de nivel se leen y se aplican todos desde el historial de créditos con un bloqueo de fila.

Permiso write:ambassadors, más sesión. Nunca en caché. Acepta Idempotency-Key. Con límite de 20 por minuto y visitante (client_ip).

CampoTipoObligatorioNotas
reward_idstringsíUn UUID
answerstringno500 caracteres como máximo, cuando la recompensa hace una pregunta

Respuesta 201 en caso de éxito, con claim_id y status.

Errores: invalid_key (401, sin sesión), not_found (404, recompensa desconocida), conflict (409, inactive, not_started, ended, out_of_stock, max_per_member, insufficient_credits), validation_error (403, member_not_active o tier_locked; 422, answer_required o cuerpo mal formado).

Dos rechazos, member_not_active y tier_locked, responden 403 con el código validation_error: lee el campo reason para distinguirlos.

POST /v1/ambassador-email-preference

El miembro con la sesión iniciada desactiva los emails del programa, o los vuelve a activar.

Permiso write:ambassadors, más sesión. Nunca en caché. Acepta Idempotency-Key. Con límite de 20 por minuto y visitante (client_ip).

CampoTipoObligatorioNotas
opt_outbooleansí

Un interruptor, no la baja de un solo sentido que hace el pie de un email: el portal es el único sitio donde un miembro puede demostrar quién es y volver a cambiar de opinión.

Errores: invalid_key (401, sesión), validation_error (422, falta opt_out o no es un booleano).

POST /v1/ambassador-logout

Revoca la sesión nombrada en X-Ambassador-Session.

Permiso write:ambassadors. Nunca en caché. Acepta Idempotency-Key.

Respuesta: siempre 200 { "ok": true }, existiera o no la sesión, y se hubiera enviado o no la cabecera: cerrar sesión no es un sitio donde aprender nada.

Errores: ninguno más allá de los de autenticación.

GET /v1/ambassador-code

Resuelve un ?amb=CODE en una página de entradas: si este código está activo en este evento, cuánto vale, y de quién es. Una vista previa, nunca un precio: las condiciones se vuelven a leer dentro del checkout desde la misma apertura.

Permiso read:ambassadors. En caché 30 s. Con límite de 60 consultas por minuto y clave.

ParámetroValoresPor defectoSignificado
event_identero positivoobligatorioEl evento contra el que se comprueba el código
codetexto, 64 caracteres como máximoobligatorioEl código tal como se escribió
{ "code": "MELANPAU", "discount_id": "f4c8b1a2-...", "ambassador_first_name": "Melanie", "percentage": 15, "amount": null, "min_order_amount": 0 }

discount_id es lo que una tienda pasa a POST /v1/checkout-sessions como discount_id: ese endpoint rechaza de plano un código de cupón en bruto. Un código que no existe, que no está en este evento, que pertenece a un miembro suspendido, o que pertenece a otro tenant, responden todos el mismo 404: cualquier cosa más específica convertiría esto en un oráculo de enumeración sobre todos los promotores de la plataforma.

Errores: validation_error (422, event_id / code ausente o mal formado), not_found (404), rate_limited (429).


MCP y webhooks

Dos formas más de llegar a los mismos datos, cada una documentada en su propia página:

  • POST /v1/mcp es el servidor MCP de Sway: los endpoints de lectura de arriba, expuestos como herramientas de solo lectura para un asistente de IA sobre JSON-RPC (HTTP en streaming, sin estado), detrás de una clave con el permiso mcp:access. Consulta El servidor MCP de Sway.
  • Webhooks, configurados en Admin del crew → API → Webhooks, envían eventos (pedidos pagados y reembolsados, eventos publicados y actualizados, solicitudes de booking, ventas de embajadores, respuestas a formularios, pagos) al propio servidor de un socio a medida que ocurren, firmados a la manera de Standard Webhooks. Consulta Webhooks.