Référence des endpoints
Tous les endpoints sont accessibles sous https://www.sway.events/api/v1 et sont limités au crew propriétaire de la clé API, envoyée comme jeton Bearer :
curl https://www.sway.events/api/v1/artists \
-H "Authorization: Bearer sway_sk_YOUR_KEY"
Les endpoints de liste renvoient l'enveloppe standard { data, pagination } et la pagination par curseur ; chaque endpoint de ressource unique renvoie l'objet ressource directement, sans enveloppe data. Voir Pagination et filtres pour les curseurs, les filtres et le mécanisme expand.
Chaque réponse 4xx ou 5xx est un problème RFC 9457 (application/problem+json) avec un champ code stable. Voir Authentification pour le cycle de vie des clés, les scopes, les limites de débit et la table complète des codes d'erreur.
Cette page documente les 80 routes qui portent sur les données propres d'un crew. Le serveur MCP et les webhooks sont des mécanismes de livraison distincts sur ces mêmes données et ont leurs propres pages ; voir la section de clôture.
Clé et crew
GET /v1/me
Introspection de la clé : le test "ma clé est-elle vivante". Renvoie la clé identifiée (id, type, scopes, limite de débit et limites de travail, sites web enregistrés) et son crew.
Aucun scope requis. Clés publiques : non. Jamais mis en cache (la réponse dépend de la clé et doit toujours être à jour).
{
"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 est le budget de travail propre à la clé (ce qu'un cache miss dépense) ; limits sont les plafonds plus larges décrits sur la page Authentification. Aucune erreur propre à la route au-delà des erreurs d'authentification standard.
GET /v1/usage
Ce que la clé appelante a consommé de ses limites en ce moment : le travail cette minute et aujourd'hui, le plafond de requêtes, les octets servis aujourd'hui, et si la clé est gelée après un motif de requêtes inhabituel.
Aucun scope requis. Clés publiques : non. Jamais mis en cache ; le lire ne dépense jamais de travail (coût 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 est le budget propre à la clé ; crew_work est ce même budget mis en commun entre toutes les clés du crew. Aucune erreur propre à la route.
GET /v1/audit-log
Les derniers appels faits par la clé, les plus récents d'abord : heure, méthode, chemin, statut, adresse de l'appelant, résultat de cache et durée. Conservés sept jours, 1 000 entrées au plus.
Aucun scope requis. Clés publiques : non. Jamais mis en cache (privacy: no-store).
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
limit | entier, 1 à 1 000 | 100 | Combien d'entrées renvoyer |
{
"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 est l'adresse observée propre à la clé appelante. cache vaut hit, miss, stale ou null quand la route ne peut pas être mise en cache.
Erreurs : validation_error (400) quand limit est hors plage.
POST /v1/keys/rotate
Remplace la clé appelante par une nouvelle : même nom, type, scopes, limites et sites web. L'ancienne clé continue de fonctionner pendant une période de grâce (pour qu'un partenaire puisse redéployer sans interruption), puis expire d'elle-même.
Scope write:keys (opt-in). Clés publiques : non. Jamais mis en cache. Ne prend pas Idempotency-Key : la réponse porte le nouveau secret, et Sway ne conserve jamais une réponse qui porte un secret, donc il n'y a rien à rejouer pour une nouvelle tentative.
| Champ | Type | Requis | Notes |
|---|---|---|---|
grace_hours | number | non | Nombre entier, 1 à 168, défaut 24 : combien de temps l'ancienne clé répond encore |
{
"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" }
}
Le nouveau secret complet (key) s'affiche une fois, dans cette réponse, exactement comme à la création : enregistrez-le immédiatement.
Erreurs : validation_error (400, corps ou grace_hours invalide), invalid_key (401, la clé avait déjà disparu), conflict (409, cette clé a déjà une remplaçante : faites plutôt tourner la clé qui l'a remplacée), rate_limited (429, une clé ne peut tourner qu'une fois par heure).
GET /v1/crew
Le profil du crew plus les ids des pages qu'il gère.
Scope read:profile. Clés publiques : oui. Mis en cache 300 s.
{
"id": "a2628d53-4f21-4c1e-9b7d-1a2b3c4d5e6f",
"name": "Insomnia",
"slug": "insomnia",
"pages": { "artists": [245, 512], "promoters": [697] }
}
Aucune erreur propre à la route.
GET /v1/crew/pages
Les pages que le crew gère (artistes, promoteurs, salles), chacune avec son nom et si elle est publiée. À la différence de toutes les autres routes, une page en brouillon est nommée ici : c'est la propre liste du crew.
Scope read:profile. Clés publiques : non. Mis en cache 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 et page_url valent null tant que la page reste non publiée. Aucune erreur propre à la route.
GET /v1/genres
La taxonomie des genres : ids et noms stables, triés par nom. Ce sont ces ids que prennent les filtres genre= de GET /v1/artists, GET /v1/events et GET /v1/events/search.
Aucun scope requis. Clés publiques : oui. Mis en cache 3600 s.
{
"data": [
{ "id": 12, "name": "Hard Groove" },
{ "id": 45, "name": "Techno" }
]
}
Aucune erreur propre à la route.
Découverte
Ces quatre routes ne demandent aucune clé API : elles décrivent l'API elle-même. Chacune est limitée à 60 requêtes par minute et par adresse appelante, au-delà de quoi la réponse est 429 rate_limited.
GET /v1/openapi.json
Cette API sous forme de document OpenAPI 3.1, généré à partir de la même liste de routes que celle sur laquelle tourne l'API, si bien qu'il ne décrit jamais une route qui n'existe pas et n'en oublie jamais une qui existe.
Aucune clé requise. Mis en cache 3600 s.
Aucune erreur propre à la route au-delà de la limite par adresse ci-dessus.
GET /v1/scopes
Chaque scope qu'une clé peut détenir : ce qu'il accorde, si une clé publique peut le détenir, si seul le propriétaire du crew peut l'accorder, et les routes qu'il ouvre.
Aucune clé requise. Mis en cache 3600 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 vraie réponse liste les dix-sept scopes et toutes les routes que chacun ouvre.
GET /v1/changelog
Ce qui a changé dans l'API, les changements les plus récents d'abord, chaque date avec les routes livrées ce jour-là.
Aucune clé requise. Mis en cache 3600 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 l'API et ce dont elle dépend répondent : la base de données, les paiements (Stripe) et l'e-mail, chacun ok ou down, sondés au plus une fois par intervalle pour que la route elle-même ne devienne jamais une charge. L'ensemble n'est ok que si chaque composant l'est.
Aucune clé requise. Jamais mis en cache.
{
"status": "ok",
"api_version": "v1",
"time": "2026-09-26T12:00:00.000Z",
"components": { "database": "ok", "payments": "ok", "email": "ok" }
}
Jamais un chiffre de latence, une version ou un message d'erreur : seulement ok ou down.
Artistes
Chaque route ci-dessous accepte une clé publique.
GET /v1/artists
Le roster : chaque artiste que le crew gère, par nom, croissant. Les artistes du roster sont renvoyés même non publiés, puisque le roster est la donnée propre du crew.
Scope read:artists. Mis en cache 300 s.
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
q | texte, 2 à 100 caractères | aucun | Le nom contient, comparé comme texte |
genre | liste d'ids de genre séparés par des virgules, 10 au plus | aucun | N'importe lequel de ces genres |
updated_since | date ou date-heure ISO 8601 | aucun | Changé à cet instant ou après |
ids | liste d'ids séparés par des virgules, 50 au plus | aucun | Seulement ces artistes |
limit | entier, 1 à 100 | 25 | Taille de page |
cursor | chaîne opaque | aucun | Depuis le next_cursor d'une page précédente |
{
"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 est réservé au niveau géré et n'apparaît jamais dans links. Erreurs : validation_error (400, q, genre, updated_since, ids ou limit invalide), invalid_cursor (400).
GET /v1/artists/{id}
Un artiste du roster, en représentation complète, renvoyé directement. Un artiste étranger (un invité du lineup non géré par le crew) n'est pas atteignable de cette façon.
Scope read:artists. Mis en cache 300 s.
Même forme qu'une entrée du roster ci-dessus. Erreurs : not_found (404, id malformé, id inconnu, ou artiste que le crew ne gère pas).
GET /v1/artists/{id}/events
L'historique et le calendrier des dates de l'artiste : événements publiés où l'artiste est un membre confirmé du lineup. Chaque élément porte une référence venue légère.
Scopes read:artists et read:events (les deux requis). Mis en cache 120 s.
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
status | upcoming, past, all | upcoming | Quels événements inclure |
limit | entier, 1 à 100 | 25 | Taille de page |
cursor | chaîne opaque | aucun | Depuis une page précédente |
Ce flux n'accepte pas 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 }
}
Erreurs : not_found (404, id malformé ou artiste que le crew ne gère pas), validation_error (400, status invalide), invalid_cursor (400).
GET /v1/artists/{id}/ics
Les prochaines dates de l'artiste sous forme de flux iCalendar (text/calendar) auquel s'abonner. 100 événements au plus, RFC 5545, lignes repliées à 75 octets, heures en UTC. GET /v1/promoters/{id}/ics et GET /v1/events/{id}/ics, plus bas, sont ce même flux pour les événements d'un promoteur et pour un seul événement.
Scopes read:artists et read:events (les deux requis). Mis en cache 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
Le fichier est nommé artist-{id}.ics (Content-Disposition: inline). Erreurs : not_found (404, id malformé, ou artiste non géré ou non publié).
GET /v1/artists/{id}/presskits
Les press kits publics de l'artiste, celui par défaut en premier. Les kits privés et les brouillons sont laissés de côté ici ; voir GET /v1/presskits pour la liste complète propre au crew.
Scopes read:artists et read:content (les deux requis). Mis en cache 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"
}
]
}
Erreurs : not_found (404, id malformé ou artiste que le crew ne gère pas).
Promoteurs
Chaque route ci-dessous accepte une clé publique.
GET /v1/promoters
Chaque promoteur que le crew gère, en représentation complète, trié par nom.
Scope read:promoters. Mis en cache 300 s.
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
q | texte, 2 à 100 caractères | aucun | Le nom contient |
ids | liste d'ids séparés par des virgules, 50 au plus | aucun | Seulement ces promoteurs |
limit | entier, 1 à 100 | 25 | Taille de page |
cursor | chaîne opaque | aucun | Depuis une page précédente |
Même enveloppe et même forme d'élément que GET /v1/artists (champs de promoteur au lieu de champs d'artiste ; pas de filtre de genre). Erreurs : validation_error (400), invalid_cursor (400).
GET /v1/promoters/{id}
Un promoteur géré et publié, en représentation complète, renvoyé directement.
Scope read:promoters. Mis en cache 300 s.
Erreurs : not_found (404, id malformé ou promoteur que le crew ne gère pas).
GET /v1/promoters/{id}/events
Événements publiés d'un promoteur géré : le flux principal sur lequel un site partenaire se construit.
Scopes read:promoters et read:events (les deux requis). Mis en cache 60 s.
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
status | upcoming, past, all | upcoming | Quels événements |
from / to | date ou date-heure ISO 8601 | aucun | À partir de / jusqu'à |
expand | liste de venue, lineup, ticket_tiers séparés par des virgules | aucun | Intégrer des ressources liées |
limit | entier, 1 à 100 | 25 | Taille de page |
cursor | chaîne opaque | aucun | Depuis une page précédente |
{
"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 est par défaut une référence légère { id, name } ; expand=venue la fait passer à l'objet complet de niveau public, et expand=lineup / expand=ticket_tiers ajoutent ces tableaux à chaque élément (mêmes formes que sur GET /v1/events/{id}). Erreurs : validation_error (400, status, from, to ou expand invalide), invalid_cursor (400), not_found (404, id malformé ou promoteur que le crew ne gère pas).
GET /v1/promoters/{id}/ics
Les prochains événements du promoteur sous forme de flux iCalendar, la même forme que GET /v1/artists/{id}/ics (voir Artistes), téléchargé sous le nom promoter-{id}.ics.
Scopes read:promoters et read:events (les deux requis). Mis en cache 300 s.
Erreurs : not_found (404, id malformé ou promoteur que le crew ne gère pas).
GET /v1/promoters/{id}/venues
Les salles où se déroulent les événements publiés du promoteur, les plus utilisées d'abord.
Scopes read:promoters et read:venues (les deux requis). Mis en cache 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
}
]
}
Erreurs : not_found (404, id malformé ou promoteur que le crew ne gère pas).
GET /v1/promoters/{id}/artists
Les artistes que le promoteur a bookés sur ses événements publiés, les plus bookés d'abord. Les artistes du roster arrivent au niveau complet, les autres au niveau public.
Scopes read:promoters et read:artists (les deux requis). Mis en cache 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 compte des événements, pas des sets : un double booking la même nuit ne compte qu'une fois. Erreurs : not_found (404, id malformé ou promoteur que le crew ne gère pas).
Événements
start_time et end_time sur une entrée du lineup, et chaque horaire dans GET /v1/events/{id}/days et GET /v1/events/{id}/timetable, restent en RFC 3339 avec le décalage UTC propre à l'événement, par exemple 22:00:00+02:00 plutôt que 22:00:00+00:00 : les chiffres sont exactement ceux que l'organisateur a tapés, et l'instant est le bon. Ils valent null tant que l'organisateur garde la timetable désactivée (event-days.ts, apiSetTime).GET /v1/events/{id}/days suit les jours que l'organisateur a déclarés dans les réglages des jours de festival, ou traite l'événement entier comme un seul jour quand aucun n'est déclaré ; unscheduled contient les sets sans horaire, ou en dehors de chaque jour déclaré. GET /v1/events/{id}/timetable suit l'ordre de scènes propre déclaré par l'organisateur, les scènes non déclarées ensuite par ordre alphabétique, et liste les sets sans scène en dernier.Chaque route ci-dessous accepte une clé publique.
GET /v1/events
Le flux union du graphe du crew : les événements de ses promoteurs, les dates confirmées de son roster, et l'agenda des salles qu'il gère. Chaque élément est un résumé léger avec une référence venue.
Scope read:events. Mis en cache 60 s.
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
status | upcoming, past, all | upcoming | Quels événements |
from / to | date ou date-heure ISO 8601 | aucun | Fenêtre de dates |
promoter_id | entier positif | aucun | Un promoteur géré |
artist_id | entier positif | aucun | Un artiste du roster |
venue_id | entier positif | aucun | Une salle du graphe |
genre | liste d'ids de genre séparés par des virgules, 10 au plus | aucun | N'importe lequel de ces genres |
city | texte, 1 à 100 caractères | aucun | Nom de ville exact |
country | ISO 3166-1 alpha-2 | aucun | Code pays |
near | lat,lng en degrés décimaux | aucun | Centre d'une recherche par rayon |
radius_km | nombre, 1 à 500 | 25 | Rayon autour de near (nécessite near) |
updated_since | date ou date-heure ISO 8601 | aucun | Changé à cet instant ou après |
has_tickets | true, false | aucun | Si l'événement a une catégorie de billets ou non |
ids | liste d'ids séparés par des virgules, 50 au plus | aucun | Seulement ces événements |
limit | entier, 1 à 100 | 25 | Taille de page |
cursor | chaîne opaque | aucun | Depuis une page précédente |
Ce flux ne supporte pas expand : le passer renvoie validation_error (400). Utilisez GET /v1/events/{id} ou GET /v1/promoters/{id}/events pour des données étendues.
{
"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 }
}
Erreurs : validation_error (400, expand fourni, valeur de filtre invalide, ou radius_km sans near), invalid_cursor (400), not_found (404, promoter_id ou artist_id nomme une page que le crew ne gère pas).
GET /v1/events/search
Le même flux que GET /v1/events, filtré aux événements dont le titre, la description ou le lineup confirmé contient le texte recherché.
Scope read:events. Mis en cache 60 s.
Chaque filtre de GET /v1/events, plus :
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
q | texte, 2 à 100 caractères | requis | Comparé comme texte au titre, à la description et au lineup |
Même enveloppe que GET /v1/events. Erreurs : validation_error (400, q manquant ou de mauvaise longueur, ou n'importe lequel des filtres partagés), invalid_cursor (400), not_found (404, promoter_id ou artist_id en dehors du graphe).
GET /v1/events/{id}
Un événement publié du graphe du crew, renvoyé directement. venue (niveau public complet) et lineup sont toujours intégrés ; genres est toujours présent.
Scope read:events. Mis en cache 60 s.
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
expand | ticket_tiers | aucun | venue et lineup sont déjà intégrés, donc c'est la seule valeur qui ajoute quelque chose |
Quand expand=ticket_tiers est présent, la réponse porte aussi un objet ticket_fee : { "percent": number, "fixed": number, "applies_to": "ticket" | "order" }, les frais de service du bénéficiaire de cet événement, en EUR. C'est le même objet que GET /v1/events/{id}/fees, qui explique comment l'appliquer.
{
"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"]
}
Les entrées du lineup portent le propre DTO de l'artiste plus stage, start_time, end_time et artist_id : les artistes du roster arrivent au niveau complet (avec contact_emails), les invités étrangers au niveau public. Un artiste personnalisé, non lié, apparaît comme une entrée de remplacement avec artist_id: null et son nom tapé sous name. La seconde entrée du lineup ci-dessus a start_time/end_time: null parce que ce set n'a pas d'horaire enregistré, pas parce que la timetable est désactivée ; voir la note au-dessus de la section.
Erreurs : validation_error (400, valeur expand inconnue), not_found (404, id malformé, événement non publié, ou événement en dehors du graphe du crew).
GET /v1/events/{id}/ticket-tiers
Les catégories de billets de l'événement : nom, description, prix, statut, fenêtre de vente et un lien profond checkout_url vers le checkout Sway. Pas de compteurs de stock, pas de commandes, pas de données acheteurs. Une petite liste fixe : enveloppée dans { "data": [...] } mais sans pagination ni curseur.
Scope read:events. Mis en cache 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 est dérivé côté serveur : on_sale, scheduled, sold_out ou off_sale. currency vaut EUR par défaut. Erreurs : not_found (404).
fee_amount est les frais de service sur un billet et price_with_fee le prix tout compris payé par l'acheteur (price plus fee_amount), selon le modèle de frais du promoteur payé, les mêmes chiffres que la page de l'événement et le MCP public. Affichez price_with_fee en premier et fee_amount comme frais de service. Sur l'ancien modèle par commande (fee_applies_to: "order"), les deux valent null : les frais dépendent de tout le panier, voir GET /v1/events/{id}/fees. Les catégories gratuites ont un fee_amount de 0.
GET /v1/events/{id}/availability
Si les gens peuvent acheter en ce moment : en vente, programmée, épuisée ou terminée, par catégorie et pour l'événement dans son ensemble, avec un indicateur "il en reste peu". Jamais un compteur de stock.
Scope read:events. Mis en cache 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 déclenche une fois que le stock restant d'une catégorie tombe à un dixième ou moins de son stock initial (minimum 5), et seulement tant qu'elle est on_sale. Le status propre à l'événement peut valoir no_tickets quand l'événement n'a aucune catégorie du tout. Erreurs : not_found (404).
GET /v1/events/{id}/fees
Les frais de service qu'un acheteur paie sur les billets de cet événement, d'après le modèle de frais du promoteur bénéficiaire : par défaut un pourcentage du prix de chaque billet payant plus un montant fixe par billet.
Scope read:events. Mis en cache 300 s.
{ "event_id": 4521, "percent": 0.03, "fixed": 0.3, "applies_to": "ticket" }
Avec applies_to: "ticket", chaque billet payant porte round(price * percent + fixed) au centime (un lot compte pour un billet, un billet gratuit ne porte rien), et total = subtotal + la somme de ces frais. Quelques promoteurs gardent un modèle plus ancien, applies_to: "order" : total = subtotal + subtotal * percent + fixed, une fois par commande payante. Dans les deux cas, le résultat correspond au total que facture le checkout Stripe. Retombe sur le taux par billet par défaut quand le bénéficiaire ne peut pas être résolu, plutôt que d'échouer. Erreurs : not_found (404).
GET /v1/events/{id}/days
Les jours que l'organisateur a déclarés dans les réglages des jours de festival de l'événement, chacun avec les sets qui y commencent ; ou l'événement comme un seul jour quand aucun n'est déclaré.
Scope read:events. Mis en cache 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 contient les sets sans horaire, ou en dehors de chaque jour déclaré. Timetable désactivée, timetable_published vaut false et chaque jour arrive avec un tableau slots vide. Un set B2B est un seul créneau avec plusieurs artists ; name est le nom personnalisé de l'organisateur quand il est défini, sinon les artistes joints par " B2B ". Erreurs : not_found (404).
GET /v1/events/{id}/timetable
Les sets programmés par scène, les scènes dans l'ordre propre déclaré par l'organisateur (les autres par ordre alphabétique, celles sans scène en dernier), chaque scène dans l'ordre de passage.
Scope read:events. Mis en cache 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 sans horaire n'est pas sur une timetable et est laissé de côté entièrement (il peut quand même apparaître dans GET /v1/events/{id}/days, sous unscheduled). Timetable désactivée, published vaut false et stages est vide. Erreurs : not_found (404).
GET /v1/events/{id}/ics
L'événement sous forme de flux iCalendar, la même forme que GET /v1/artists/{id}/ics (voir Artistes), pour un bouton "ajouter au calendrier". Téléchargé sous le nom event-{id}.ics.
Scope read:events. Mis en cache 300 s.
Erreurs : not_found (404, id malformé, événement non publié ou en dehors du graphe du crew, ou événement sans date de début).
GET /v1/events/{id}/schema-org
L'événement sous forme de JSON-LD schema.org (MusicEvent), prêt à injecter tel quel dans le <script type="application/ld+json"> d'une page partenaire, pour les moteurs de recherche.
Scope read:events. Mis en cache 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 est construit à partir des sets de la timetable (pas du lineup brut), donc il liste chaque nom une seule fois et suit la même règle de "timetable désactivée" que les routes ci-dessus. Erreurs : not_found (404).
GET /v1/events/{id}/gallery
Les albums photo publics de l'événement, chacun avec les photos dont l'upload est terminé (les 200 premières) et son nombre total de photos.
Scope read:events. Mis en cache 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
}
]
}
Les albums privés et les photos encore en cours d'upload n'apparaissent jamais. Erreurs : not_found (404).
GET /v1/events/{id}/partners
Les partenaires des promoteurs et salles de l'événement, dans l'ordre d'affichage : la même forme que GET /v1/partners (voir Contenu), enveloppée sous { "event_id": 4521, "data": [...] }.
Scopes read:events et read:content (les deux requis). Mis en cache 300 s.
Erreurs : not_found (404, événement non publié ou en dehors du graphe du crew).
GET /v1/events/{id}/news
Les actus publiées de l'événement, les plus récentes d'abord, en markdown et en HTML nettoyé : la même forme que GET /v1/news (voir Contenu), enveloppée sous event_id.
Scopes read:events et read:content (les deux requis). Mis en cache 120 s.
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
limit | entier, 1 à 100 | 25 | Taille de page |
cursor | chaîne opaque | aucun | Depuis une page précédente |
Erreurs : not_found (404, événement non publié ou en dehors du graphe du crew), invalid_cursor (400).
GET /v1/events/{id}/presales
Les préventes de l'événement qui ne sont pas des brouillons, exactement comme les montre la page de prévente publique : teaser, fenêtre d'inscription, si l'inscription est ouverte en ce moment.
Scope read:events. Mis en cache 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-..."
}
]
}
Les produits, plafonds, règle de sélection, promoteur et liste de diffusion derrière une prévente ne sont jamais exposés. Erreurs : not_found (404).
Salles
Chaque route ci-dessous accepte une clé publique.
GET /v1/venues
Les salles du graphe du crew : celles qu'il gère (managed: true) et celles qui accueillent ses événements publiés, niveau public, par nom.
Scope read:venues. Mis en cache 300 s.
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
q | texte, 2 à 100 caractères | aucun | Le nom contient |
ids | liste d'ids séparés par des virgules, 50 au plus | aucun | Seulement ces salles |
limit | entier, 1 à 100 | 25 | Taille de page |
cursor | chaîne opaque | aucun | Depuis une page précédente |
{
"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 }
}
Erreurs : validation_error (400), invalid_cursor (400).
GET /v1/venues/{id}
Une salle du graphe du crew, niveau public, avec sa capacité, renvoyée directement. Atteignable seulement quand le crew la gère ou qu'elle accueille un événement publié du graphe.
Scope read:venues. Mis en cache 300 s.
Même forme d'élément que ci-dessus (avec managed). Erreurs : not_found (404).
GET /v1/venues/{id}/events
L'agenda de la salle : chaque événement publié du graphe du crew qui s'y déroule (tous quand le crew gère la salle). Mêmes filtres que GET /v1/events, sans les filtres de page/artiste/salle.
Scopes read:venues et read:events (les deux requis). Mis en cache 60 s.
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
status | upcoming, past, all | upcoming | Quels événements |
from / to | date ou date-heure ISO 8601 | aucun | Fenêtre de dates |
limit | entier, 1 à 100 | 25 | Taille de page |
cursor | chaîne opaque | aucun | Depuis une page précédente |
Même enveloppe et même forme d'élément que GET /v1/events. Erreurs : not_found (404, id malformé ou salle en dehors du graphe), validation_error (400), invalid_cursor (400).
GET /v1/venues/{id}/capacity
Combien de personnes la salle accueille par configuration de salle (debout, assis, théâtre, cabaret, banquet, cocktail, classe), et le chiffre le plus élevé : les configurations sont des agencements alternatifs de la même salle, jamais des zones qui s'additionnent.
Scope read:venues. Mis en cache 300 s.
{ "venue_id": 88, "max": 300, "layouts": { "standing": 300, "seated": 120 } }
max et layouts valent null et {} quand la salle ne déclare aucune capacité. Erreurs : not_found (404).
Contenu
Chaque route ci-dessous accepte une clé publique.
GET /v1/news
Les actus publiées d'une page que le crew gère (un promoteur, une salle ou un artiste), les plus récentes d'abord, en markdown et en HTML nettoyé. Un post programmé pour plus tard reste de côté jusqu'à son heure. GET /v1/events/{id}/news (voir Événements) est ce même flux pour un événement plutôt qu'une page.
Scope read:content. Mis en cache 120 s.
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
promoter_id / venue_id / artist_id | entier positif | aucun | Exactement un, nommant une page que le crew gère |
limit | entier, 1 à 100 | 25 | Taille de page |
cursor | chaîne opaque | aucun | Depuis une page précédente |
{
"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 }
}
Erreurs : validation_error (400, pas exactement un de promoter_id / venue_id / artist_id), invalid_cursor (400), not_found (404, la page nommée n'est pas gérée par le crew).
GET /v1/partners
Les partenaires d'un promoteur ou d'une salle que le crew gère, dans l'ordre d'affichage.
Scope read:content. Mis en cache 300 s.
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
promoter_id / venue_id | entier positif | aucun | Exactement un, nommant une page que le crew gère |
{
"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 } }
]
}
Erreurs : validation_error (400, pas exactement un de promoter_id / venue_id), not_found (404, la page nommée n'est pas gérée par le crew).
GET /v1/presskits
Les press kits de chaque artiste que le crew gère, avec leur visibilité (public, privé, brouillon) et si un mot de passe les protège. Une clé publique ne voit que les publics ; une clé secrète voit les trois états.
Scope read:content. Mis en cache 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" }
]
}
Aucune erreur propre à la route.
GET /v1/presskits/{id}
Un press kit exactement comme le montre sa propre page : l'artiste, les sections dans l'ordre choisi, leurs textes et fichiers. Un kit protégé demande son mot de passe dans l'en-tête Presskit-Password, de la part de tout appelant, crew gestionnaire compris.
Scope read:content. Jamais mis en cache (la réponse dépend d'un en-tête que le cache ne voit pas).
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 privé ne s'ouvre qu'à la clé secrète du crew gestionnaire ; un brouillon n'est jamais atteignable. Erreurs : not_found (404, id malformé, kit introuvable, artiste non géré, kit en brouillon, ou kit privé appelé avec le mauvais type de clé), password_required (401, mot de passe manquant ou incorrect), rate_limited (429, 10 tentatives de mot de passe par minute au plus, par clé et par kit, et par adresse de visiteur quand l'appel se fait avec une clé publique).
GET /v1/forms
Les formulaires du crew et des pages qu'il gère, brouillons laissés de côté, chacun disant s'il prend des réponses en ce moment.
Scope read:content. Mis en cache 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 vaut draft, scheduled, open, full, closed ou empty (un formulaire publié sans aucune question). Erreurs : aucune au-delà de l'authentification.
GET /v1/forms/{slug}
Les questions, le design et la disponibilité d'un formulaire, pour le rendre sur le site partenaire, exactement comme le ferait sa propre page publique.
Scope read:content. Mis en cache 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 et design sont la définition du formulaire telle que le form builder la stocke, transmise telle quelle : les réponses que vous postez à POST /v1/forms/{slug}/submit sont indexées par les ids de question à l'intérieur de steps. captures_contact dit si une adresse donnée dans le formulaire va dans les contacts de l'organisateur.
Erreurs : not_found (404, forme de slug invalide, formulaire en brouillon, ou formulaire n'appartenant ni au crew ni à l'une de ses pages).
Export
Aucune de ces routes n'accepte de clé publique ; chacune demande read:export plus le scope de lecture correspondant. Jamais mises en cache (no-store), et chacune est freinée à un export par ressource et par clé toutes les cinq minutes, au-delà de quoi la réponse est rate_limited (429). Chaque fichier est plafonné à 10 000 lignes et dit s'il a été coupé avec un en-tête X-Export-Truncated: true|false.
GET /v1/export/events
GET /v1/export/artists
GET /v1/export/promoters
GET /v1/export/venues
Le catalogue publié du crew, une ressource à la fois, en JSON délimité par des sauts de ligne (application/x-ndjson) : un objet JSON par ligne, sans enveloppe, sans pagination. Chaque ligne est exactement le DTO que rend la route de liste correspondante (GET /v1/events, GET /v1/artists, GET /v1/promoters, GET /v1/venues), donc un export ne montre jamais plus que ce que l'API montre déjà.
Scopes read:export + read:events / read:artists / read:promoters / read:venues respectivement.
{"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 réponse est téléchargée sous le nom sway-events.ndjson, sway-artists.ndjson, sway-promoters.ndjson ou sway-venues.ndjson (Content-Disposition: attachment). Erreurs : rate_limited (429, un export de cette ressource a déjà tourné pour cette clé dans les cinq dernières minutes).
Demandes de booking
POST /v1/booking-requests
Soumet une demande de booking depuis un site partenaire dans le pipeline de booking du crew et notifie le crew. Chaque artiste demandé doit être un artiste géré du roster.
Scope write:bookings (opt-in). Clés publiques : non. Jamais mis en cache. Prend Idempotency-Key. Limité en débit plus strictement que les lectures : 10 requêtes par minute et par visiteur (client_ip), et 300 par jour et par crew.
| Champ | Type | Requis | Notes |
|---|---|---|---|
artist_ids | number[] | oui | Non vide, 50 ids au plus, chacun un artiste géré du roster |
contact_email | string | oui | Email valide, 320 caractères au plus |
contact_name | string | non | 200 caractères au plus |
contact_phone | string | non | 200 caractères au plus |
event_name | string | non | 200 caractères au plus |
event_date | string | non | Date ISO, YYYY-MM-DD |
venue | string | non | 200 caractères au plus |
city | string | non | 200 caractères au plus |
fee_offer | string | non | Texte libre, 200 caractères au plus |
capacity | string | non | Texte libre, 200 caractères au plus |
ticket_price | string | non | Texte libre, 200 caractères au plus |
organization | string | non | 200 caractères au plus |
{
"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"
}
Réponse 201 Created, renvoyée directement :
{ "id": "9f2c1a7e-4d3b-4e21-8c7a-1b2c3d4e5f60", "status": "new", "created_at": "2026-07-03T14:00:00.000Z" }
Erreurs : validation_error (422, forme de corps invalide, ou artist_ids nommant un id en dehors du roster, listé sous errors[].invalid_ids), rate_limited (429, limite par visiteur ou par crew et par jour).
Audience
Aucune de ces routes n'accepte de clé publique.
POST /v1/newsletter-signup
Ajoute un e-mail à la newsletter d'un promoteur géré en tant que contact CRM, avec une entrée de journal de consentement RGPD.
Scope write:newsletter (opt-in). Jamais mis en cache. Prend Idempotency-Key. Limité à 10 requêtes par minute et par visiteur (client_ip), et 500 inscriptions par jour et par promoteur.
| Champ | Type | Requis | Notes |
|---|---|---|---|
email | string | oui | Email valide, 320 caractères au plus |
promoter_id | number | oui | Un promoteur que le crew gère |
first_name / last_name | string | non | 200 caractères au plus |
locale | string | non | 10 caractères au plus |
country | string | non | 2 caractères au plus |
{ "email": "[email protected]", "promoter_id": 697, "first_name": "Jane" }
Réponse 201 Created : { "status": "subscribed" }.
Erreurs : validation_error (422, corps invalide, ou promoter_id non géré par le crew), rate_limited (429).
POST /v1/newsletter-unsubscribe
Retire un e-mail de la newsletter d'un promoteur géré et de son prestataire de liste de diffusion : même statut CRM que respecte toute synchronisation ultérieure.
Scope write:newsletter (opt-in). Jamais mis en cache. Prend Idempotency-Key. Limité à 10 requêtes par minute et par visiteur (client_ip), et 500 par jour et par promoteur.
| Champ | Type | Requis | Notes |
|---|---|---|---|
email | string | oui | Email valide |
promoter_id | number | oui | Un promoteur que le crew gère |
Réponse 200 OK : toujours { "status": "unsubscribed" }, que l'adresse ait été sur la liste ou non : la route ne doit jamais dire à un appelant qui est inscrit.
Erreurs : validation_error (422, corps invalide, ou promoter_id non géré), rate_limited (429).
POST /v1/forms/{slug}/submit
Envoie les réponses d'un formulaire que le site partenaire a rendu à partir de GET /v1/forms/{slug} : validées contre la définition stockée, classées dans le CRM du propriétaire quand le formulaire capture un contact, avec api:form:{slug} enregistré comme source du consentement.
Scope write:forms (opt-in). Jamais mis en cache. Prend Idempotency-Key. Limité à 60 soumissions par minute et par clé et par formulaire, 5 par minute et par adresse de visiteur (quand client_ip est transmis), et 5 000 par jour et par formulaire.
| Champ | Type | Requis | Notes |
|---|---|---|---|
answers | object | oui | Indexé par id de champ, comme les nomme la propre définition du formulaire |
locale | string | non | Tag de langue à deux lettres, avec une région en option |
honeypot | string | non | Un champ caché du propre formulaire du partenaire : le moindre texte dedans et la réponse est silencieusement abandonnée |
client_ip | string | non | L'adresse du visiteur, utilisée seulement pour compter ses soumissions |
{ "answers": { "email": "[email protected]", "s1_q1": "Yes" } }
Réponse 201 Created : { "status": "received", "redirect_url": null } (un honeypot rempli répond exactement la même chose, mais ne stocke rien).
Erreurs : not_found (404, slug invalide, formulaire en brouillon, ou formulaire n'appartenant pas au crew), validation_error (422, corps malformé ou réponses qui échouent à la propre validation du formulaire, sous errors[].param comme answers.<field>), conflict (409, not_taking_answers quand le formulaire n'est pas ouvert en ce moment, already_answered quand le formulaire n'autorise qu'une réponse par adresse et que celle-ci a déjà répondu), rate_limited (429).
Paiement (checkout)
Aucune de ces routes n'accepte de clé publique.
POST /v1/checkout-sessions
Crée une session de paiement hébergé Stripe pour une vitrine publique et renvoie son url. L'acheteur y est redirigé, puis revient sur la page /success ou /cancel propre à Sway, jamais sur le site de l'appelant ; la finalisation (commande, billets, e-mail de confirmation) est entièrement du ressort de Sway.
Scope write:checkout (opt-in) ; un panier dont le total est exactement zéro demande en plus write:checkout:free. Jamais mis en cache. Ne prend pas Idempotency-Key : cette route garde son propre garde-fou à la place (idempotency_key dans le corps, plus bas). Limité à 60 requêtes par minute et par acheteur (client_ip), 600 par minute et par clé, 5 000 paiements par jour et par crew, et 30 paiements payants (ou 10 gratuits) par jour et par e-mail acheteur.
Tout ce qui fait autorité sur l'argent et le stock est résolu côté serveur et ne peut jamais être fourni ni surchargé par l'appelant : le prix, la devise, le compte Stripe du bénéficiaire, les frais de service et la réservation de stock. Le crew de la clé doit gérer le promoteur qui reçoit les fonds, sinon validation_error.
| Champ | Type | Requis | Notes |
|---|---|---|---|
event_id | number | oui | Doit être publié et dans le graphe du crew |
line_items | array | oui | 1 à 20 entrées de { "tier_id": string, "quantity": number } ; tier_id est un id de catégorie venant de GET /v1/events/{id}/ticket-tiers, quantity de 1 à 50 |
buyer_email | string | oui | Email valide, 320 caractères au plus : où les billets sont livrés |
idempotency_key | string | non | Un UUID par clic d'achat ; une nouvelle tentative avec la même valeur renvoie la même session |
discount_id | string | non | Un id de coupon pour l'événement, venant de POST /v1/coupons/validate |
access_code | string | non | Un code de prévente ou d'accès qui débloque une catégorie cachée |
reference | string | non | Texte libre, 64 caractères au plus de [A-Za-z0-9._:-], recopié vers l'analytics uniquement |
locale | string | non | La langue de l'acheteur (en, fr, nl...), utilisée pour nommer la ligne de frais de service sur la page de paiement Stripe. Par défaut l'Accept-Language de la requête, puis l'anglais |
marketing_consent | boolean | non | Ignoré sur ce chemin ; un consentement affirmé par une clé API n'est jamais retenu pour une inscription CRM |
consent | object | non | Des booléens { analytics, marketing } plus version/at, transmis uniquement à l'enregistrement d'attribution publicitaire |
client_ip / client_user_agent / event_source_url | string | non | Signaux côté acheteur pour le rapprochement d'attribution publicitaire, et client_ip pour compter les checkouts de l'acheteur. Jamais utilisés pour autre chose |
Champs rejetés avec validation_error s'ils sont fournis, parce que chacun est résolu côté serveur : amount / price / unit_amount, currency, tout bénéficiaire ou 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"
}
Réponse 200 OK, renvoyée directement :
{ "url": "https://checkout.stripe.com/c/pay/cs_live_...", "checkout_id": "chk_9f2c1a7e..." }
checkout_id n'accompagne qu'une session payante ; une commande gratuite (comp) renvoie { "url": ... } seul, puisqu'il n'y a ensuite aucune session Stripe à suivre.
Un checkout payant retient ses billets, ainsi qu'un usage de son coupon et de son code d'accès, pendant les 30 minutes de la session. Ce que l'acheteur ne paie pas revient à l'expiration de la session ; d'ici là, un code dont le dernier usage est retenu répond validation_error.
write:checkout:free. Billets gratuits et payants ne peuvent pas être combinés dans une même commande.Erreurs : missing_scope (403, pas de write:checkout, ou commande gratuite sans write:checkout:free), not_found (404, événement non publié ou en dehors du graphe), validation_error (422, corps invalide, catégorie inconnue ou indisponible, quantité au-dessus de max_per_order, lignes gratuites et payantes mélangées, ou bénéficiaire non géré par le crew), conflict (409, le compte de paiement du promoteur n'est pas encore prêt), rate_limited (429, voir Retry-After), upstream_error (502, Stripe n'a pas pu créer la session).
GET /v1/checkout-sessions/{id}
Où en est un paiement ouvert par ce crew, adressé par le checkout_id que sa création a renvoyé, jamais l'id de session Stripe : ouvert (avec son lien de paiement), terminé ou expiré.
Scope write:checkout. Jamais mis en cache (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 n'est présent que tant que status vaut open. Erreurs : not_found (404, identifiant malformé, ou paiement scellé pour un autre crew).
POST /v1/checkout-sessions/{id}/expire
Termine un paiement ouvert par ce crew et rend ses billets au stock immédiatement, plutôt que d'attendre la propre minuterie de Stripe. Idempotent : l'appeler à nouveau sur un paiement déjà expiré répond simplement avec son état courant.
Scope write:checkout. Jamais mis en cache. Prend Idempotency-Key.
Même forme de réponse que GET /v1/checkout-sessions/{id}. Erreurs : not_found (404, comme ci-dessus), conflict (409, le paiement était déjà payé : un remboursement passe par l'organisateur à la place).
POST /v1/coupons/validate
Vérifie un code promo avant le paiement : le discount_id à transmettre, ses conditions, et les totaux du panier quand il en envoie un, ou pourquoi il ne s'applique pas. Répond 200 dans tous les cas : un code refusé est une réponse, pas une erreur.
Scope write:checkout. Jamais mis en cache. Ne prend pas Idempotency-Key (writeClass: check) : elle s'exécute à chaque fois, jusqu'à 300 vérifications par minute et par clé, en plus des limites de devinette ci-dessous.
| Champ | Type | Requis | Notes |
|---|---|---|---|
event_id | number | oui | Doit être un événement dont le crew gère le bénéficiaire |
code | string | oui | 1 à 64 caractères imprimables |
line_items | array | non | Le panier du paiement, même forme que POST /v1/checkout-sessions |
client_ip | string | non | L'adresse de l'acheteur, utilisée seulement pour compter ses échecs |
{ "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 code appartenant à un autre événement, à un autre organisateur, ou désactivé, répondent tous, sans distinction, { "valid": false, "code": "...", "reason": "not_found" }. Autres valeurs de reason : min_order (avec min_order_amount), product_limit (avec products_remaining), expired, usage_limit, not_open_here, not_applicable. L'id de coupon Stripe ne sort jamais.
Erreurs : validation_error (422, corps invalide), not_found (404, événement non publié ou en dehors du graphe), validation_error (422, bénéficiaire non géré par le crew), rate_limited (429, le plafond de vérifications par minute, ou la limite de devinette ci-dessous).
POST /v1/access-codes/validate
Vérifie un code de prévente ou d'accès : les catégories de billets cachées qu'il débloque (jamais listées par GET /v1/events/{id}/ticket-tiers), si l'organisateur veut que seules ces catégories soient montrées, et le plafond par commande propre au code, ou pourquoi il ne fonctionne pas.
Scope write:checkout. Jamais mis en cache. Ne prend pas Idempotency-Key (writeClass: check), même plafond de 300 vérifications par minute que les coupons.
| Champ | Type | Requis | Notes |
|---|---|---|---|
event_id | number | oui | Doit être un événement dont le crew gère le bénéficiaire |
code | string | oui | 1 à 64 caractères imprimables |
client_ip | string | non | L'adresse de l'acheteur, utilisée seulement pour compter ses échecs |
{
"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 veut dire que l'organisateur veut que seules ces catégories débloquées soient montrées une fois le code saisi. Les ids internes propres au code ne sortent jamais. Autres valeurs de reason de refus : not_found, inactive, not_started, ended, usage_limit.
Limites de devinette (les deux routes ci-dessus). Au-delà du plafond général de vérifications par minute, un code inconnu est compté par clé et par événement (300 échecs par 10 minutes) et, quand l'appelant transmet client_ip, par adresse acheteuse aussi (10 échecs par 15 minutes) : au-delà de l'une ou l'autre limite, la réponse est rate_limited (429) avant même que le code soit recherché.
Erreurs : les mêmes que POST /v1/coupons/validate.
Boutique (shop)
Les trois lectures acceptent une clé publique ; le devis et le paiement, non.
Le merch vendu par les pages que votre crew gère, pour une boutique à vous : le catalogue d'une page, un produit, ce que vend un événement, le prix d'un panier, et un paiement Stripe sur le compte de la page qui vend. L'acheteur paie sur Stripe et revient sur les pages de Sway ; Sway enregistre la commande, écrit à l'acheteur et au vendeur, et le vendeur gère la livraison, les rétractations et les remboursements depuis l'écran Shop de la page.
Une page vend dès que sa boutique est ouverte ; avant, son catalogue se lit vide. Les prix sont TTC, en centimes d'euro. Aucune route ne donne de stock : une variante est available ou non.
GET /v1/shop/products
Le merch publié d'une page du crew, avec l'identité du vendeur que l'acheteur doit voir avant de payer. Les exclusivités d'événement n'y sont pas : lisez-les sur l'événement.
Scope read:shop. Clés publiques : oui. Mis en cache 30 s.
| Paramètre | Type | Requis | Notes |
|---|---|---|---|
seller_type | string | oui | promoter, artist ou venue |
seller_id | number | oui | Une page que le crew gère |
{
"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 }
}
]
}
Une petite liste : enveloppée dans { "data": [...] }, sans pagination. Une page que le crew ne gère pas, ou dont la boutique n'est pas ouverte, répond { "seller": null, "data": [] }. fulfilment_modes liste delivery, pickup_event et pickup_location ; country_rule vaut all, only ou except, appliqué à countries. Erreurs : validation_error (422, seller_type ou seller_id absent ou invalide).
preorder vaut null, ou { "status": "open", "ships_on": "2026-11-15", "ends_on": null } pour un produit vendu avant d'être prêt : il est expédié à partir de ships_on et prend des précommandes jusqu'à ends_on quand il est fixé. status vaut closed quand la précommande s'est terminée avant le jour d'expédition ; ses variantes lisent alors available: false. Le jour de l'expédition, le produit se vend comme les autres et preorder redevient null.
installments vaut null, ou l'offre de paiement en plusieurs fois de Stripe sur le compte de la page qui vend. Quand il est présent, affichez le Payment Method Messaging Element de Stripe avec publishable_key et stripe_account (Stripe.js), jamais un texte à vous : la publicité du crédit fractionné est encadrée. methods liste ce que le Checkout propose (klarna) ; country est celui du vendeur, une valeur par défaut tant que l'acheteur n'a pas choisi de pays de livraison. Le produit et le shop d'un événement portent le même champ.
GET /v1/shop/products/{id}
Un produit pour sa page produit, renvoyé directement, avec seller qui porte l'identité complète du vendeur. Avec event_id, le produit tel que cet événement le vend, exclusivités comprises.
Scope read:shop. Clés publiques : oui. Mis en cache 30 s.
| Paramètre | Type | Requis | Notes |
|---|---|---|---|
event_id | number | non | Un événement du graphe du crew dont la boutique est ouverte |
Erreurs : not_found (404, produit inconnu ou non publié, page que le crew ne gère pas, exclusivité demandée sans son événement, ou événement dont la boutique est fermée).
GET /v1/events/{id}/shop
Ce que vend un événement du graphe du crew : les produits de son organisateur payé, tous ou une sélection, exclusivités comprises, tant que la boutique de l'événement est ouverte ; et si l'acheteur peut encore choisir le retrait à l'événement.
Scope read:shop. Clés publiques : oui. Mis en cache 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 porte l'identité complète du vendeur, comme dans la liste plus haut. La liste est vide, avec seller et pickup_at_event à null, tant que la boutique de l'événement est fermée, ou quand l'organisateur payé n'est pas une page que le crew gère, puisque le paiement ne pourrait pas vendre pour lui. Erreurs : not_found (404, événement non publié ou hors du graphe).
POST /v1/shop/quote
Chiffre un panier avant le paiement : les lignes après le code promo, les options de livraison pour un mode et un pays, les modes et pays que le panier permet, et le total. Rien n'est réservé.
Scope read:shop. Clés publiques : non. Jamais mis en cache. Limité à 120 requêtes par minute et par acheteur (client_ip).
| Champ | Type | Requis | Notes |
|---|---|---|---|
lines | array | oui | 1 à 20 entrées { "variant_id": string, "quantity": number }, quantité de 1 à 20 |
mode | string | oui | delivery, pickup_event ou pickup_location |
event_id | number | non | L'événement d'où vient le panier : il débloque ses exclusivités et le retrait à l'événement |
country | string | pour delivery | Code à deux lettres du pays de livraison |
coupon_code | string | non | Un code de la page qui vend |
method_key | string | non | L'option de livraison choisie, prise dans shipping_options |
client_ip | string | non | L'adresse de l'acheteur, pour compter ses requêtes |
{
"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 est le jour d'expédition de la commande quand le panier contient une précommande, le plus tardif d'entre eux ; l'acheteur le voit avant de payer, y compris sur la page Stripe. Il vaut null pour un panier en stock. not_ready_for_event refuse le retrait à l'événement d'un panier expédié après lui ; preorder_closed porte le variant_id dont la précommande est terminée.
Erreurs : validation_error (422) avec une 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, ou une raison qui commence par coupon_. max_per_order porte variant_id et max ; un refus de livraison porte available_modes et deliverable_countries. Puis conflict (409, raison not_ready : la page ne peut pas encore vendre), not_found (404, événement hors du graphe) et rate_limited (429).
POST /v1/shop/cart-check
Indique quelles lignes du panier qu'un acheteur garde pointent vers un produit qui a quitté le shop : dépublié, archivé ou supprimé, ou une variante désactivée ou retirée. Retirez ces lignes du panier. Une variante épuisée, une page qui ne peut pas encore vendre ou un événement dont le shop est fermé garde sa ligne : cela peut revenir, et le devis l'explique. Une ligne d'une page que votre crew ne gère pas revient aussi.
Scope read:shop. Clés publiques : non. Jamais mis en cache. Limité à 120 requêtes par minute et par acheteur (client_ip).
| Champ | Type | Requis | Notes |
|---|---|---|---|
lines | array | oui | 1 à 20 entrées { "variant_id": string, "product_id": string } |
client_ip | string | non | L'adresse de l'acheteur, pour compter ses requêtes |
{ "gone": ["9a1b7c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d"] }
Vérifiez le panier quand l'acheteur revient sur le site et quand le panier s'ouvre.
Erreurs : validation_error (422, une ligne absente ou mal formée) et rate_limited (429).
POST /v1/shop/checkout-sessions
Ouvre un paiement Stripe hébergé pour le merch d'une page que le crew gère, sur le compte Stripe de cette page, et renvoie son url. L'acheteur y paie, puis arrive sur la page de remerciement de Sway et sur sa page de commande, ou sur /cancel, jamais sur le site de l'appelant. Sway enregistre la commande, écrit à l'acheteur et au vendeur, et prélève sa commission chez le vendeur.
Scope write:shop:checkout (à activer). Clés publiques : non. Jamais mis en cache. N'accepte pas Idempotency-Key : envoyez idempotency_key dans le corps. Limité à 60 requêtes par minute et par acheteur (client_ip), 5000 paiements par jour et par crew, et 30 par jour et par e-mail d'acheteur.
Le corps est celui du devis, plus :
| Champ | Type | Requis | Notes |
|---|---|---|---|
buyer_email | string | oui | Où partent la confirmation et le lien de la commande |
idempotency_key | string | non | Un UUID par clic sur Payer ; un nouvel essai avec la même valeur renvoie la même session |
locale | string | non | La langue de l'acheteur pour Stripe et les e-mails : en, fr, de, es, it, nl ou uk |
reference | string | non | Libre, 64 caractères au plus parmi [A-Za-z0-9._:-], gardée sur la commande |
Champs refusés avec validation_error s'ils sont envoyés, car chacun est résolu côté serveur : 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"
}
Réponse 200 OK : { "url": "https://checkout.stripe.com/c/pay/cs_live_..." }
Le stock est tenu pendant les 30 minutes de la session ; ce que l'acheteur ne paie pas revient quand la session expire. Un consentement marketing envoyé par l'appelant est ignoré : une clé d'API n'inscrit jamais un acheteur dans un CRM.
Erreurs : celles du devis, plus validation_error pour un buyer_email ou une reference invalide, ou pour des produits d'une page que le crew ne gère pas ; conflict (409, not_ready, ou le stock a bougé sous le panier) ; rate_limited (429, voir Retry-After) ; upstream_error (502, Stripe n'a pas pu créer la session).
Ambassadeurs
Le programme de parrainage du promoteur, rendu sur le propre site du partenaire plutôt que sur sway.events. Aucune de ces routes n'accepte de clé publique : exécutez-les depuis un serveur, jamais un navigateur. Trois identifiants entrent en jeu : la clé API sway_sk_... (quel crew appelle), une session membre amb_st_... voyageant dans l'en-tête X-Ambassador-Session (quel ambassadeur est connecté, émise à l'inscription ou à la connexion, 8 heures), et le cookie first-party propre au partenaire qui relie un visiteur à cette session.
GET /v1/ambassador-program
Tout ce qu'il faut à une page "devenir ambassadeur" : le nom et la description du programme, la remise qu'il propose, s'il accepte du monde en ce moment, et les mots propres au promoteur pour XP, paliers, quêtes et badges. Données publiques uniquement, aucun membre et aucun décompte de membres.
Scope read:ambassadors. Mis en cache 120 s.
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
owner_type | promoter, venue | requis | Le programme de qui |
owner_id | entier positif | requis | L'id de la page |
{
"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 réponse répète aussi name, description, signup_open et gamification_enabled au premier niveau, pour les sites construits sur la première version du boilerplate Insomnia. Lisez les champs imbriqués : ce sont eux que cette page décrit.
Un owner en dehors du graphe du crew répond le même 404 qu'un owner sans programme : cette route ne doit jamais dire lesquels des promoteurs que la clé ne gère pas font tourner un programme.
Erreurs : validation_error (422, owner_type / owner_id manquant ou malformé : cette route est antérieure à la règle du 400 pour les paramètres de requête et garde sa réponse), not_found (404).
GET /v1/ambassador-leaderboard
Le classement du programme, seulement quand le promoteur le publie : rang, prénom et initiale du nom, score. Un classement par revenu donne l'ordre, jamais les montants.
Scope read:ambassadors. Mis en cache 60 s.
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
owner_type | promoter, venue | requis | Le programme de qui |
owner_id | entier positif | requis | L'id de la page |
limit | entier, 1 à 100 | 25 | Combien de rangs |
{
"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 vaut tickets, xp ou revenue. Avec revenue, score vaut null : l'ordre montre qui a le plus vendu, jamais combien. tier n'est défini que tant que les paliers du programme sont actifs.
Erreurs : validation_error (400, owner_type / owner_id manquant ou malformé), not_found (404, pas de programme, ou le programme ne publie pas son classement).
GET /v1/ambassador-rewards
Les récompenses sur lesquelles les membres dépensent leurs crédits : coût, ce qu'il en reste, quand, et le palier requis. Vide tant que les crédits sont désactivés.
Scope read:ambassadors. Mis en cache 60 s.
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
owner_type | promoter, venue | requis | Le programme de qui |
owner_id | entier positif | requis | L'id de la page |
{
"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 vaut null quand la récompense n'a pas de limite de stock. available vaut false en dehors de ses dates ou une fois épuisée. claim_prompt est la question à laquelle le membre répond en la réclamant (le answer de POST /v1/ambassador-claim).
Erreurs : validation_error (400, owner_type / owner_id), not_found (404, pas de programme).
GET /v1/ambassador-quests
Les quêtes que les membres peuvent entreprendre maintenant : objectif, dates et récompenses. La propre progression d'un membre connecté est dans GET /v1/ambassador-me, jamais ici.
Scope read:ambassadors. Mis en cache 60 s.
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
owner_type | promoter, venue | requis | Le programme de qui |
owner_id | entier positif | requis | L'id de la page |
{
"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 est ce dont le membre doit atteindre threshold : orders, tickets, revenue_cents, credits_earned, reward_claimed, profile_complete ou manual (l'organisateur la coche lui-même). scope vaut lifetime (depuis que le membre a rejoint), event (sur un événement, ou sur n'importe quel événement seul quand event est nul) ou window (entre starts_at et ends_at). data est vide tant que la gamification est désactivée.
Erreurs : validation_error (400, owner_type / owner_id), not_found (404, pas de programme).
GET /v1/ambassadors
Les membres du programme pour les propres outils du promoteur, les plus récents d'abord : nom, e-mail, Instagram, code, statut, ventes, crédits, XP et palier. Jamais le téléphone, le jeton d'accès ou le compte Sway.
Scope read:ambassador-members (réservé au propriétaire : seul le propriétaire du crew peut l'accorder). Jamais mis en cache (no-store).
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
owner_type | promoter, venue | requis | Le programme de qui |
owner_id | entier positif | requis | L'id de la page |
q | texte | aucun | Recherche sur le nom, l'e-mail, l'Instagram ou le code |
status | active, pending, suspended | aucun | Filtrer par statut |
limit | entier, 1 à 100 | 25 | Taille de page |
cursor | chaîne opaque | aucun | Depuis une page précédente |
{
"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 }
}
Erreurs : validation_error (400, owner_type / owner_id / status), invalid_cursor (400), not_found (404, pas de programme pour cet owner).
GET /v1/ambassadors/{id}/ledger
L'historique de crédits d'un membre, le plus récent d'abord, avec son solde courant : ce que chaque vente, réclamation, quête ou attribution manuelle a ajouté ou retiré.
Scope read:ambassador-members. Jamais mis en cache.
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
limit | entier, 1 à 100 | 25 | Taille de page |
cursor | chaîne opaque | aucun | Depuis une page précédente |
{
"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 }
}
L'owner que le crew doit gérer est lu depuis le propre programme du membre, jamais depuis la requête : un membre d'un autre crew répond le même 404 qu'un id inconnu. Erreurs : not_found (404, id malformé ou membre en dehors des programmes du crew), invalid_cursor (400).
GET /v1/ambassador-stats
Comment se porte le programme : membres par statut et part de ceux actifs qui vendent, les ventes que ses codes ont apportées (depuis toujours, et entre from et to quand ils sont fournis), et les dix meilleurs codes sur la même période.
Scope read:ambassador-members. Jamais mis en cache.
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
owner_type | promoter, venue | requis | Le programme de qui |
owner_id | entier positif | requis | L'id de la page |
from / to | date ou date-heure ISO 8601 | aucun | Fenêtre, 400 jours d'écart au plus, to après 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": []
}
Erreurs : validation_error (400, owner_type / owner_id, ou fenêtre inversée ou de plus de 400 jours), not_found (404, pas de programme).
POST /v1/ambassador-register
Quelqu'un rejoint le programme depuis le propre site du partenaire. Renvoie son code et une session membre, pour que la vitrine puisse lui montrer sa page immédiatement.
Scope write:ambassadors (opt-in). Jamais mis en cache. Prend Idempotency-Key. Limité à 30 inscriptions par minute et par visiteur (client_ip, dimensionné pour le wifi partagé d'une salle), et 300 par jour et par programme.
| Champ | Type | Requis | Notes |
|---|---|---|---|
owner_type | string | oui | promoter ou venue |
owner_id | number | oui | L'id de la page |
first_name / last_name | string | oui | 120 caractères au plus |
email | string | oui | Email valide ; le seul chemin de retour vers une page perdue |
phone | string | oui | Un vrai numéro de téléphone ; la clé de déduplication du programme |
locale | string | non | 8 caractères au plus |
{ "owner_type": "promoter", "owner_id": 697, "first_name": "Jane", "last_name": "Doe", "email": "[email protected]", "phone": "+32470000000" }
Réponse : 201 pour un membre réellement nouveau, 200 quand il existait déjà :
{ "created": true, "member_id": "6b1d...", "code": "MELANPAU", "status": "active", "session": { "token": "amb_st_...", "expires_at": "2026-09-01T18:00:00.000Z" } }
Un numéro de téléphone déjà inscrit dans ce programme est refusé, pas connecté : celui qui a tapé le numéro n'est pas forcément celui à qui il appartient.
Erreurs : validation_error (422, champs manquants ou invalides, ou e-mail jetable/bloqué), not_found (404, owner en dehors du graphe du crew), conflict (409, signup_closed tant que le promoteur n'a pas activé l'inscription publique et qu'une invitation site web vivante n'a plus de quota, ou already_registered pour un téléphone déjà inscrit), rate_limited (429).
POST /v1/ambassador-login
Envoie par e-mail à un membre un lien de connexion à usage unique qui retombe sur le propre site du partenaire.
Scope write:ambassadors. Jamais mis en cache. Prend Idempotency-Key. Limité à 8 par minute et par visiteur (client_ip), et 5 par heure et par adresse e-mail pour l'envoi effectif.
| Champ | Type | Requis | Notes |
|---|---|---|---|
owner_type | string | oui | promoter ou venue |
owner_id | number | oui | L'id de la page |
email | string | oui | Où envoyer le lien |
return_origin | string | oui | Une des origines enregistrées sur cette clé |
return_path | string | non | Un chemin relatif sur cette origine, défaut /ambassador |
Réponse 202, toujours le même corps : { "status": "accepted" }. Une adresse inconnue, un membre suspendu, un programme fermé et un envoi réel sont indistinguables : un formulaire de connexion qui répondrait différemment pour une adresse connue serait un oracle d'appartenance. return_origin et return_path sont revalidés en SQL, donc il n'y a aucun moyen d'assembler une redirection ouverte.
Erreurs : validation_error (422, corps invalide, ou return_origin absent des origines enregistrées de la clé), rate_limited (429, par visiteur ou par adresse e-mail).
POST /v1/ambassador-login-exchange
La page d'atterrissage du partenaire reçoit ?amb_token=... et l'échange, côté serveur, contre une session membre.
Scope write:ambassadors. Jamais mis en cache. Prend Idempotency-Key. Limité à 20 tentatives par minute et par visiteur (client_ip).
| Champ | Type | Requis | Notes |
|---|---|---|---|
token | string | oui | La valeur amb_lt_... de la chaîne de requête |
{ "session": { "token": "amb_st_...", "expires_at": "2026-09-01T18:00:00.000Z" }, "return_url": "https://www.example.com/ambassador" }
Usage unique : le jeton est dépensé par la même mise à jour de base de données qui le lit, si bien que deux clics dans un client mail qui précharge les liens ne peuvent pas réussir tous les deux.
Erreurs : invalid_key (401, jeton malformé, inconnu, expiré ou déjà utilisé, tous identiques), rate_limited (429).
GET /v1/ambassador-me
La propre page du membre connecté : code, ventes, crédits et récompenses, plus XP, palier, quêtes et badges quand le promoteur a activé la gamification.
Scope read:ambassadors, plus un en-tête X-Ambassador-Session. Jamais mis en cache.
{
"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 même page membre que montre sway.events, raccourcie ici : game porte aussi les mots propres au programme (tier_noun, quest_noun, badge_noun) et next_tier dit combien d'XP il reste à grimper (null en haut). quests, badges, tiers et xp_ledger ne se remplissent que tant que la gamification est active, rewards, claims et ledger (l'historique de crédits) seulement tant que les crédits sont actifs. events liste les événements sur lesquels le code du membre fonctionne, avec sa remise, et leaderboard le classement quand le promoteur le publie, avec revenue_cents sur chaque rang uniquement dans un classement par revenu, comme sur sway.events.
access_token (l'identifiant permanent du membre pour sa page sur sway.events) et son numéro de téléphone n'apparaissent jamais ici, quoi que contienne la ligne en dessous.
Erreurs : invalid_key (401, en-tête manquant ou malformé, ou session expirée ou invalide).
POST /v1/ambassador-profile
Le membre connecté corrige son propre nom, e-mail ou langue.
Scope write:ambassadors, plus session. Jamais mis en cache. Prend Idempotency-Key. Limité à 20 par minute et par visiteur (client_ip).
| Champ | Type | Requis | Notes |
|---|---|---|---|
first_name / last_name / email / locale | string | non | N'importe quel sous-ensemble ; au moins un est requis |
Le numéro de téléphone ne peut pas être changé ici (c'est la clé de déduplication : le changer serait une fusion, pas une modification), pas plus que le statut, le code, les crédits ou l'XP : le code ne suit pas un changement de nom.
Erreurs : invalid_key (401, session), validation_error (422, e-mail invalide, aucun champ fourni, ou e-mail jetable/bloqué), rate_limited (429).
POST /v1/ambassador-claim
Le membre connecté dépense des crédits sur une récompense. L'appelant n'énonce jamais un prix : coût, solde, stock, limite par membre et palier requis sont tous lus et appliqués depuis le registre sous verrou de ligne.
Scope write:ambassadors, plus session. Jamais mis en cache. Prend Idempotency-Key. Limité à 20 par minute et par visiteur (client_ip).
| Champ | Type | Requis | Notes |
|---|---|---|---|
reward_id | string | oui | Un UUID |
answer | string | non | 500 caractères au plus, quand la récompense pose une question |
Réponse 201 en cas de succès, portant claim_id et status.
Erreurs : invalid_key (401, pas de session), not_found (404, récompense inconnue), conflict (409, inactive, not_started, ended, out_of_stock, max_per_member, insufficient_credits), validation_error (403, member_not_active ou tier_locked ; 422, answer_required ou corps malformé).
member_not_active et tier_locked, répondent 403 avec le code validation_error : lisez le champ reason pour les distinguer.POST /v1/ambassador-email-preference
Le membre connecté désactive les e-mails du programme, ou les réactive.
Scope write:ambassadors, plus session. Jamais mis en cache. Prend Idempotency-Key. Limité à 20 par minute et par visiteur (client_ip).
| Champ | Type | Requis | Notes |
|---|---|---|---|
opt_out | boolean | oui |
Un interrupteur, pas le désabonnement à sens unique d'un pied de page d'e-mail : le portail est le seul endroit où un membre peut prouver qui il est et revenir sur sa décision.
Erreurs : invalid_key (401, session), validation_error (422, opt_out manquant ou n'étant pas un booléen).
POST /v1/ambassador-logout
Révoque la session nommée dans X-Ambassador-Session.
Scope write:ambassadors. Jamais mis en cache. Prend Idempotency-Key.
Réponse : toujours 200 { "ok": true }, que la session ait existé ou non, et que l'en-tête ait même été envoyé ou non : se déconnecter n'est pas un endroit où apprendre quoi que ce soit.
Erreurs : aucune au-delà de l'authentification.
GET /v1/ambassador-code
Résout un ?amb=CODE sur une page de billets : ce code est-il actif sur cet événement, que vaut-il, et à qui appartient-il. Un aperçu, jamais un prix : les conditions sont relues à l'intérieur du paiement depuis la même ouverture.
Scope read:ambassadors. Mis en cache 30 s. Limité à 60 recherches par minute et par clé.
| Paramètre | Valeurs | Défaut | Signification |
|---|---|---|---|
event_id | entier positif | requis | L'événement contre lequel le code est vérifié |
code | texte, 64 caractères au plus | requis | Le code tel que saisi |
{ "code": "MELANPAU", "discount_id": "f4c8b1a2-...", "ambassador_first_name": "Melanie", "percentage": 15, "amount": null, "min_order_amount": 0 }
discount_id est ce qu'une vitrine transmet à POST /v1/checkout-sessions comme discount_id : cette route refuse purement et simplement un code de coupon brut. Un code qui n'existe pas, qui n'est pas sur cet événement, qui appartient à un membre suspendu, ou qui appartient à un autre tenant, répondent tous le même 404 : quoi que ce soit de plus précis en ferait un oracle d'énumération sur tous les promoteurs de la plateforme.
Erreurs : validation_error (422, event_id / code manquant ou malformé), not_found (404), rate_limited (429).
MCP et webhooks
Deux autres façons d'atteindre les mêmes données, chacune documentée sur sa propre page :
POST /v1/mcpest le serveur MCP de Sway : les routes de lecture ci-dessus, exposées comme des outils en lecture seule pour un assistant IA par JSON-RPC (streamable HTTP, sans état), derrière une clé détenant le scopemcp:access. Voir Le serveur MCP de Sway.- Les webhooks, configurés dans Admin crew → API → Webhooks, poussent les événements (commandes payées et remboursées, événements publiés et modifiés, demandes de booking, ventes ambassadeurs, réponses de formulaire, versements) vers le propre serveur d'un partenaire au moment où ils se produisent, signés à la manière Standard Webhooks. Voir Webhooks.