Autenticación
Cada petición lleva una clave de API como token Bearer:
curl https://www.sway.events/api/v1/artists \
-H "Authorization: Bearer sway_sk_YOUR_KEY"
La clave identifica a tu crew. No hay ningún parámetro de tenant en toda la API: lo que una clave puede ver lo decide el crew al que pertenece, nunca la petición.
Dos tipos de clave
| Clave secreta | Clave publicable | |
|---|---|---|
| Prefijo | sway_sk_ | sway_pk_ |
| Se ejecuta en | Tu servidor | El navegador de un visitante |
| Permisos | Cualquiera | Solo las lecturas públicas: read:profile, read:artists, read:promoters, read:events, read:venues, read:content, read:shop |
| Dónde responde | En cualquier sitio | Solo en los sitios web listados en la clave (se comprueba contra el Origin del navegador) |
| Se envía como | Authorization: Bearer ... | Authorization: Bearer ..., o ?key= cuando una cabecera no es práctica |
Usa una clave secreta para todo lo que hace tu servidor, y para cualquier cosa que escriba, venda entradas o toque a los miembros. Nunca la envíes a un navegador ni a una app: quien pueda leer tus páginas puede leer la clave.
Usa una clave publicable cuando un navegador tenga que llamar a la API directamente, por ejemplo un sitio estático sin servidor. Solo lee lo que Sway ya muestra públicamente, solo responde en sus propios sitios web, y recibe una cuota más pequeña por dirección de visitante, encima de la propia de la clave.
?key= se rechaza con 400 key_in_query, porque las URL acaban en logs e historiales del navegador. Si pasa, trata la clave como filtrada: rótala.Ciclo de vida de una clave
Las claves se gestionan en Admin del crew → API (planes Studio y Roster, permiso Gestionar API).
| Acción | Qué pasa |
|---|---|
| Crear | Nombre, tipo, permisos, sitios web o direcciones de la tienda, y una caducidad (nunca, 30, 90 o 365 días). La clave completa se muestra una vez; después solo se ven su inicio y sus cuatro últimos caracteres |
| Editar | Cambia los permisos y las listas de direcciones; se aplica al instante |
| Suspender | El interruptor de emergencia. Toda petición responde 403 key_suspended hasta que alguien hace clic en Reanudar. No se elimina nada |
| Rotar | POST /v1/keys/rotate con una clave que tenga write:keys devuelve su sustituta: mismo nombre, tipo, permisos y límites. La clave antigua sigue funcionando durante grace_hours (de 1 a 168, 24 por defecto), y luego caduca. Una clave rota una vez, como mucho una vez por hora, y se avisa por email al propietario del crew cada vez |
| Revocar | Definitivo. Las peticiones responden 401 invalid_key. Cuando la clave se rotó, También revocar las claves que la sustituyeron revoca toda la cadena: úsalo cuando la clave se haya filtrado, ya que quien tenga una clave filtrada con write:keys podría haberla rotado |
| Caducar | Pasado expires_at, las peticiones responden 401 invalid_key |
Una clave revocada, caducada o desconocida responden todas lo mismo, 401 invalid_key: la API nunca dice a quien llama si una clave que no conoce llegó a existir alguna vez. El panel Actividad de cada clave muestra lo que ha usado de sus límites y sus últimas llamadas.
Si la suscripción del crew caduca, todas las claves responden 403 subscription_required hasta que vuelva a estar activa.
Permisos
Los permisos se eligen al crear la clave y se pueden editar después. Una petición sin el permiso que exige su endpoint responde 403 missing_scope.
| Permiso | Abre | Por defecto |
|---|---|---|
read:profile | El perfil propio del crew: /v1/crew, /v1/crew/pages | Sí |
read:artists | El roster, cada artista, sus eventos, calendarios y press kits | Sí |
read:promoters | Los promotores que gestionas, sus eventos, salas y artistas | Sí |
read:events | Eventos, line-ups, días, horarios, tipos de entrada, disponibilidad, comisiones, galería, preventas | Sí |
read:venues | Salas, sus eventos y aforo | Sí |
read:content | Noticias, partners, formularios y press kits | Sí |
read:ambassadors | La página pública del programa de embajadores, su clasificación, sus recompensas y misiones, y la página propia de un miembro con sesión iniciada | No |
read:ambassador-members | Los miembros, sus movimientos y las estadísticas del programa. Datos personales: solo el propietario del crew | No |
read:export | Todo el catálogo como descargas NDJSON | No |
read:shop | El merch de las páginas que gestionas: catálogo, producto, lo que vende un evento; el cálculo y la comprobación de un carrito | No |
write:bookings | Enviar solicitudes de booking (POST /v1/booking-requests) | No |
write:newsletter | Añadir altas a la newsletter al CRM de un promotor, y retirar una dirección | No |
write:forms | Enviar respuestas a tus formularios públicos | No |
write:checkout | Abrir checkouts de entradas de pago, y comprobar cupones y códigos de acceso | No |
write:checkout:free | Emitir también pedidos de entradas gratis o con 100% de descuento. Solo el propietario del crew | No |
write:shop:checkout | Abrir checkouts de merch para una página que gestionas | No |
write:ambassadors | Unirse, iniciar y cerrar sesión, canjear una recompensa, editar sus propios datos | No |
write:keys | Dejar que la clave se rote a sí misma (POST /v1/keys/rotate) | No |
mcp:access | Usar la clave con el servidor MCP | No |
GET /v1/me, /v1/usage, /v1/audit-log y /v1/genres no necesitan ningún permiso: cualquier clave válida puede llamarlos.
Los permisos "solo el propietario del crew" solo puede concederlos el propietario del crew; un miembro con el permiso Gestionar API puede quitarlos pero no añadirlos. GET /v1/scopes devuelve esta lista en JSON.
Sitios web y direcciones de la tienda
En una clave viven dos listas de direcciones, las dos se configuran en Admin del crew → API:
- Sitios web (claves publicables): los únicos orígenes de navegador en los que responde la clave. Una petición desde otro sitio responde
403 origin_not_allowed. - Direcciones de la tienda (claves con un permiso de embajadores): a dónde puede devolver Sway a un miembro tras un enlace de acceso por email. Tu web pasa la dirección de vuelta con la petición; Sway solo la acepta si su origen está en la lista, así el enlace nunca se puede desviar a otro sitio. Una clave con un permiso de embajadores y la lista vacía sigue sirviendo páginas, pero nunca podrá volver a iniciar la sesión de un miembro.
Solo se aceptan orígenes https, más http en localhost para el desarrollo. Las rutas se descartan: solo se guarda y compara el origen.
Límites de peticiones
La API está pensada para que una web de socio con mucho tráfico siga mostrando sus datos. Dos ideas lo sostienen:
- Una respuesta en caché es gratis. Las lecturas se guardan en caché por crew. Repetir una petición que la caché ya tiene no cuesta nada, y un
304 Not Modifiedtampoco. - Se cuenta el trabajo, no las peticiones. Solo una petición que tiene que leer la base de datos gasta el presupuesto de trabajo de la clave, y lo gasta en proporción a lo que lee.
| Plan | Trabajo por minuto | Trabajo por día |
|---|---|---|
| Studio | 120 | 20.000 |
| Roster | 300 | 100.000 |
Lo que cuesta una petición sin caché:
| Petición | Unidades de trabajo |
|---|---|
Un detalle (/v1/events/{id}) | 1 |
| Una página de lista | 1 por cada bloque empezado de 25 elementos (limit=100 cuesta 4) |
Cada valor de expand | +1 |
Una consulta por lotes (ids=) | 1 por cada bloque empezado de 10 ids |
Cuando se acaba el presupuesto, una lectura que la caché ya tiene se sigue respondiendo, desde la última copia, con Cache-Status: sway; hit; ...; detail=rate-limited. Solo una lectura sin ninguna copia responde 429 rate_limited, con Retry-After.
Otras cuotas, por clave salvo que se diga lo contrario:
| Cuota | Valor |
|---|---|
| Peticiones, en caché o no | 2.400 por minuto en Studio, 6.000 en Roster (o 20 veces el trabajo por minuto de la clave, lo que sea mayor) |
| Datos de respuesta | 5 GB al día en Studio, 20 GB en Roster |
| Todas las claves del crew juntas | 600 de trabajo por minuto y 120.000 al día en Studio; 1.500 y 500.000 en Roster. Otras tantas escrituras |
Escrituras (POST), salvo el checkout | Tantas como el trabajo por minuto de la clave, nunca menos de 60 |
| Sesiones de checkout | 600 por minuto, para la clave y para todas las claves del crew juntas, aparte de las demás escrituras |
| Comprobaciones de cupón y código de acceso | 300 por minuto, aparte de las escrituras |
| Mensajes MCP | 120 por minuto |
| Clave publicable, por dirección de visitante | 60 de trabajo y 600 peticiones por minuto |
Toda respuesta autenticada informa del presupuesto de trabajo de la clave:
RateLimit-Policy: "burst";q=120;w=60, "daily";q=20000;w=86400
RateLimit: "burst";r=118;t=42, "daily";r=19873;t=51230
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 1759525489
RateLimit-Policy y RateLimit siguen el draft de la IETF; las cabeceras X-RateLimit-* muestran la más estricta de las dos ventanas y se mantienen para los clientes existentes. Ante un 429, espera los segundos de Retry-After; una clave que sigue chocando con el límite recibe esperas más largas. Una clave que recorre ids con un patrón inusual queda limitada a respuestas en caché durante un tiempo.
If-None-Match y la caché, una web de socio que sirve a miles de visitantes al día se queda muy por debajo de estas cifras. La guía de integración muestra el patrón. Si necesitas más, contáctanos: los límites se ajustan por clave.Caché
Cada lectura cacheable lleva su tiempo de vida y un ETag:
Cache-Control: private, max-age=60, stale-while-revalidate=120, stale-if-error=86400
ETag: "4f9a1c7e2b8d5f3a"
Cache-Status: sway; hit; ttl=42
Devuelve el ETag como If-None-Match: si no ha cambiado nada, la respuesta es 304 Not Modified, sin cuerpo y sin coste. Cache-Status dice de dónde viene una respuesta: hit (la caché), fwd=miss (lectura nueva), fwd=stale (renovada después de agotarse su tiempo de vida). Los tiempos de vida van de 30 segundos (tipos de entrada, disponibilidad) a una hora (géneros); la referencia de endpoints da el de cada uno. Las respuestas que llevan datos a nivel de clave o de miembro nunca se guardan en caché (Cache-Control: private, no-store).
Para saltarte la copia de Sway, envía Cache-Control: no-cache con una clave secreta: la petición se lee fresca (Cache-Status: sway; fwd=request; stored) y cuesta trabajo como cualquier petición sin caché. Esa es la petición que hay que hacer cuando un webhook dice que algo ha cambiado, ya que la copia puede ser anterior al cambio. Cuando se te acaba el presupuesto de trabajo, la copia te llega de todos modos, con detail=rate-limited. El Cache-Control de una clave publicable se ignora.
Errores
Todo error es un cuerpo RFC 9457 application/problem+json con un code estable sobre el que decidir, y el id de la petición:
{
"type": "https://www.sway.events/en/docs/api/errors#missing_scope",
"title": "Missing Scope",
"status": 403,
"detail": "This API key does not have the required scope.",
"instance": "/api/v1/events/4521",
"code": "missing_scope",
"request_id": "0b7c2e4a-9f1d-4a3b-8c6e-5d2f1a9b7e3c"
}
code | Estado | Significado |
|---|---|---|
invalid_key | 401 | La clave está mal formada, es desconocida, fue revocada o caducó |
key_in_query | 400 | Se envió una clave secreta como ?key=. Rótala |
key_suspended | 403 | La clave está suspendida; un admin del crew puede reanudarla |
subscription_required | 403 | El plan del crew ya no incluye la API |
missing_scope | 403 | A la clave le falta el permiso del endpoint, o una clave publicable llamó a un endpoint que exige una clave secreta |
origin_not_allowed | 403 | Se usó una clave publicable desde un sitio web que no está en su lista, o un navegador llamó al servidor MCP |
not_found | 404 | El recurso no existe o está fuera de las páginas de tu crew |
conflict | 409 | La misma Idempotency-Key todavía se está procesando, o la acción ya se hizo (una clave rota una vez) |
validation_error | 400 / 422 | Un parámetro de consulta (400) o el cuerpo de la petición (422) no es válido |
invalid_cursor | 400 | El cursor de paginación está mal formado o caducado |
idempotency_mismatch | 422 | Esta Idempotency-Key se usó con otro cuerpo |
password_required | 401 | Un press kit protegido necesita Presskit-Password |
rate_limited | 429 | Se alcanzó un límite; mira Retry-After |
internal_error | 500 | Algo falló de nuestro lado; se puede reintentar sin riesgo |
upstream_error | 502 | Falló un servicio del que depende Sway (pagos, email); se puede reintentar sin riesgo |
validation_error lleva además un array errors de { "param", "message" }. Los endpoints añaden sus propios códigos donde los necesitan (un tipo de entrada agotado, un código que no aplica); la referencia de endpoints los lista.
404 not_found vale tanto para "no existe" como para "no es tuyo": la API nunca confirma que exista un dato fuera de tu crew.
Toda respuesta, de éxito o de error, lleva X-Request-Id. Cítalo cuando nos contactes: encuentra la petición en nuestros logs. Puedes enviar tu propio X-Request-Id (de 8 a 128 letras, dígitos, ., _, : o -) para enlazar nuestros logs con los tuyos; cualquier otra cosa se sustituye por un id nuevo.
Idempotencia
Un reintento de red nunca debe reservar, dar de alta o reembolsar dos veces. Envía una cabecera Idempotency-Key en un POST, un valor único por operación (un UUID es ideal, de 1 a 255 caracteres ASCII visibles):
curl -X POST https://www.sway.events/api/v1/booking-requests \
-H "Authorization: Bearer sway_sk_YOUR_KEY" \
-H "Idempotency-Key: 5b8f3c1e-2a7d-4e9b-8c6f-1d3a5e7b9c2f" \
-H "Content-Type: application/json" \
-d '{ "artist_ids": [501], "event_name": "Warehouse Series #4", "contact_email": "[email protected]" }'
- El mismo valor de
Idempotency-Keycon el mismo cuerpo, dentro de las 24 horas, recibe de vuelta la primera respuesta, sin repetir el trabajo. - El mismo valor con otro cuerpo responde
422 idempotency_mismatch. - Un reintento que llega mientras la primera petición todavía se está procesando responde
409 conflict: espera y reintenta.
Funciona en todos los POST salvo abrir un checkout, que se protege de duplicados por su cuenta, y las comprobaciones de cupón y código de acceso, que no cambian nada. Una respuesta que lleva un secreto (una clave rotada) nunca se guarda para repetirla.