Endpunkt-Referenz

Jede Route der öffentlichen Sway-API v1, gruppiert nach Ressource, mit ihren Scopes, Parametern und ihrer Antwortform.

Alle Endpunkte liegen unter https://www.sway.events/api/v1 und sind auf die Crew beschränkt, der der API-Schlüssel gehört, gesendet als Bearer-Token:

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

Listen-Endpunkte liefern den Standardumschlag { data, pagination } und Cursor-Paginierung; jeder Endpunkt für eine einzelne Ressource liefert das Ressourcenobjekt direkt, ohne data-Hülle. Siehe Paginierung und Filter für Cursor, Filter und den expand-Mechanismus.

Jede 4xx- oder 5xx-Antwort ist ein RFC-9457-Problem (application/problem+json) mit einem stabilen Feld code. Siehe Authentifizierung für den Lebenszyklus der Schlüssel, Scopes, Rate Limits und die vollständige Fehlercode-Tabelle.

Diese Seite dokumentiert die 80 Routen, die die eigenen Daten einer Crew liefern. Der MCP-Server und Webhooks sind eigene Zustellwege über dieselben Daten und haben ihre eigenen Seiten; siehe den Abschnitt am Ende.


Schlüssel & Crew

GET /v1/me

Introspektion des Schlüssels: der Test "lebt mein Schlüssel noch". Liefert den erkannten Schlüssel (ID, Typ, Scopes, Rate Limit und Arbeits-Limits, registrierte Websites) und seine Crew.

Kein Scope nötig. Veröffentlichbare Schlüssel: nein. Nie zwischengespeichert (die Antwort ist schlüsselspezifisch und muss immer aktuell sein).

{
  "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 ist das eigene Arbeitsbudget des Schlüssels (was ein Cache-Miss kostet); limits sind die weiter gefassten Obergrenzen, beschrieben auf der Seite Authentifizierung. Keine routenspezifischen Fehler über die Standard-Authentifizierungsfehler hinaus.

GET /v1/usage

Was der aufrufende Schlüssel gerade von seinen Limits verbraucht hat: Arbeit in dieser Minute und heute, die Obergrenze der Anfragen, heute ausgelieferte Bytes, und ob der Schlüssel nach einem ungewöhnlichen Anfragemuster eingefroren ist.

Kein Scope nötig. Veröffentlichbare Schlüssel: nein. Nie zwischengespeichert; das Lesen kostet nie Arbeit (Kosten 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 ist das eigene Budget des Schlüssels; crew_work ist dasselbe Budget, gebündelt über jeden Schlüssel der Crew. Keine routenspezifischen Fehler.

GET /v1/audit-log

Die letzten Aufrufe des Schlüssels, neueste zuerst: Zeit, Methode, Pfad, Status, aufrufende Adresse, Cache-Ergebnis und Dauer. Sieben Tage aufbewahrt, höchstens 1.000 Einträge.

Kein Scope nötig. Veröffentlichbare Schlüssel: nein. Nie zwischengespeichert (privacy: no-store).

ParameterWerteStandardBedeutung
limitGanzzahl, 1 bis 1.000100Wie viele Einträge zurückgegeben werden
{
  "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 ist die beobachtete eigene Adresse des aufrufenden Schlüssels. cache ist hit, miss, stale oder null, wenn die Route nicht zwischenspeicherbar ist.

Fehler: validation_error (400), wenn limit außerhalb des Bereichs liegt.

POST /v1/keys/rotate

Ersetzt den aufrufenden Schlüssel durch einen neuen: gleicher Name, Typ, Scopes, Limits und Websites. Der alte Schlüssel funktioniert noch für eine Karenzzeit (damit ein Partner ohne Lücke neu deployen kann), dann läuft er von selbst ab.

Scope write:keys (Opt-in). Veröffentlichbare Schlüssel: nein. Nie zwischengespeichert. Nimmt keinen Idempotency-Key an: Die Antwort trägt das neue Secret, und Sway speichert nie eine Antwort, die ein Secret enthält, also gibt es nichts, das sich einem Wiederholungsversuch erneut ausliefern ließe.

FeldTypPflichtHinweise
grace_hoursnumberneinGanze Zahl, 1 bis 168, Standard 24: wie lange der alte Schlüssel noch antwortet
{
  "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" }
}

Das vollständige neue Secret (key) wird einmal gezeigt, in dieser Antwort, genau wie beim Anlegen: speichere es sofort.

Fehler: validation_error (400, fehlerhafter Body oder grace_hours), invalid_key (401, der Schlüssel war schon weg), conflict (409, dieser Schlüssel hat schon einen Nachfolger: rotiere stattdessen den Schlüssel, der ihn ersetzt hat), rate_limited (429, ein Schlüssel darf einmal pro Stunde rotieren).

GET /v1/crew

Das Profil der Crew plus die IDs der Seiten, die sie verwaltet.

Scope read:profile. Veröffentlichbare Schlüssel: ja. 300 s zwischengespeichert.

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

Keine routenspezifischen Fehler.

GET /v1/crew/pages

Die Seiten, die die Crew verwaltet (Künstler, Veranstalter, Veranstaltungsorte), jede mit ihrem Namen und ob sie veröffentlicht ist. Anders als bei jeder anderen Route wird hier auch eine Entwurfsseite genannt: Es ist die eigene Liste der Crew.

Scope read:profile. Veröffentlichbare Schlüssel: nein. 300 s zwischengespeichert.

{
  "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 und page_url sind null, solange die Seite unveröffentlicht bleibt. Keine routenspezifischen Fehler.

GET /v1/genres

Die Genre-Taxonomie: stabile IDs und Namen, nach Namen sortiert. Diese IDs sind es, was die genre=-Filter von GET /v1/artists, GET /v1/events und GET /v1/events/search erwarten.

Kein Scope nötig. Veröffentlichbare Schlüssel: ja. 3600 s zwischengespeichert.

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

Keine routenspezifischen Fehler.


Discovery

Diese vier Routen brauchen überhaupt keinen API-Schlüssel: Sie beschreiben die API selbst. Jede ist auf 60 Anfragen pro Minute und aufrufende Adresse begrenzt, danach beantwortet mit 429 rate_limited.

GET /v1/openapi.json

Diese API als OpenAPI-3.1-Dokument, erzeugt aus derselben Liste von Routen, auf der die API läuft, sodass es nie eine Route beschreibt, die nicht existiert, oder eine auslässt, die es tut.

Kein Schlüssel nötig. 3600 s zwischengespeichert.

Keine routenspezifischen Fehler über das Adress-Limit oben hinaus.

GET /v1/scopes

Jeder Scope, den ein Schlüssel halten kann: was er gewährt, ob ein veröffentlichbarer Schlüssel ihn halten darf, ob ihn nur der Crew-Eigentümer erteilen darf, und die Routen, die er öffnet.

Kein Schlüssel nötig. 3600 s zwischengespeichert.

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

Die echte Antwort listet alle siebzehn Scopes und jede Route, die jeder von ihnen öffnet.

GET /v1/changelog

Was sich in der API geändert hat, neueste zuerst, jedes Datum mit den Routen, die an diesem Tag auslieferten.

Kein Schlüssel nötig. 3600 s zwischengespeichert.

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

Ob die API und das, wovon sie abhängt, antworten: die Datenbank, Zahlungen (Stripe) und E-Mail, jede ok oder down, höchstens einmal pro Intervall geprüft, damit die Route selbst nie zur Last wird. Das Ganze ist nur ok, wenn es jede Komponente ist.

Kein Schlüssel nötig. Nie zwischengespeichert.

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

Nie eine Latenzzahl, eine Version oder eine Fehlermeldung: nur ok oder down.


Künstler

Jede Route unten akzeptiert einen veröffentlichbaren Schlüssel.

GET /v1/artists

Das Roster: jeder Künstler, den die Crew verwaltet, nach Namen aufsteigend. Roster-Künstler kommen auch unveröffentlicht zurück, da das Roster die eigenen Daten der Crew sind.

Scope read:artists. 300 s zwischengespeichert.

ParameterWerteStandardBedeutung
qText, 2 bis 100 ZeichenkeinerName enthält, als Text abgeglichen
genrekommagetrennte Liste von Genre-IDs, höchstens 10keinerJedes dieser Genres
updated_sinceISO-8601-Datum oder -ZeitpunktkeinerGeändert zu oder nach diesem Zeitpunkt
idskommagetrennte Liste von IDs, höchstens 50keinerNur diese Künstler
limitGanzzahl, 1 bis 10025Seitengröße
cursoropake ZeichenfolgekeinerAus next_cursor einer vorigen Seite
{
  "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 gibt es nur in der verwalteten Stufe und erscheint nie innerhalb von links. Fehler: validation_error (400, fehlerhaftes q, genre, updated_since, ids oder limit), invalid_cursor (400).

GET /v1/artists/{id}

Ein Künstler des Rosters, volle Darstellung, direkt geliefert. Ein fremder Künstler (ein Lineup-Gast, der nicht von der Crew verwaltet wird) ist auf diesem Weg nicht adressierbar.

Scope read:artists. 300 s zwischengespeichert.

Gleiche Form wie ein Roster-Eintrag oben. Fehler: not_found (404, fehlerhaft geformte ID, unbekannte ID, oder ein Künstler, den die Crew nicht verwaltet).

GET /v1/artists/{id}/events

Die Gig-Historie und der Kalender des Künstlers: veröffentlichte Veranstaltungen, bei denen der Künstler ein bestätigtes Lineup-Mitglied ist. Jeder Eintrag trägt eine leichte venue-Referenz.

Scopes read:artists und read:events (beide nötig). 120 s zwischengespeichert.

ParameterWerteStandardBedeutung
statusupcoming, past, allupcomingWelche Veranstaltungen einzuschließen sind
limitGanzzahl, 1 bis 10025Seitengröße
cursoropake ZeichenfolgekeinerAus einer vorigen Seite

Dieser Feed akzeptiert kein from, to oder 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 }
}

Fehler: not_found (404, fehlerhaft geformte ID oder ein Künstler, den die Crew nicht verwaltet), validation_error (400, fehlerhaftes status), invalid_cursor (400).

GET /v1/artists/{id}/ics

Die kommenden Termine des Künstlers als abonnierbarer iCalendar-Feed (text/calendar). Höchstens 100 Veranstaltungen, RFC 5545, Zeilen bei 75 Oktetten gefaltet, Zeiten in UTC. GET /v1/promoters/{id}/ics und GET /v1/events/{id}/ics, weiter unten, sind derselbe Feed für die Veranstaltungen eines Veranstalters und für eine einzelne Veranstaltung.

Scopes read:artists und read:events (beide nötig). 300 s zwischengespeichert.

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

Die Datei heißt artist-{id}.ics (Content-Disposition: inline). Fehler: not_found (404, fehlerhaft geformte ID, oder der Künstler wird nicht verwaltet oder ist nicht veröffentlicht).

GET /v1/artists/{id}/presskits

Die öffentlichen Pressekits des Künstlers, das Standard-Kit zuerst. Private Kits und Entwürfe bleiben hier außen vor; siehe GET /v1/presskits für die vollständige eigene Liste der Crew.

Scopes read:artists und read:content (beide nötig). 300 s zwischengespeichert.

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

Fehler: not_found (404, fehlerhaft geformte ID oder ein Künstler, den die Crew nicht verwaltet).


Veranstalter

Jede Route unten akzeptiert einen veröffentlichbaren Schlüssel.

GET /v1/promoters

Jeder Veranstalter, den die Crew verwaltet, volle Darstellung, nach Namen sortiert.

Scope read:promoters. 300 s zwischengespeichert.

ParameterWerteStandardBedeutung
qText, 2 bis 100 ZeichenkeinerName enthält
idskommagetrennte Liste von IDs, höchstens 50keinerNur diese Veranstalter
limitGanzzahl, 1 bis 10025Seitengröße
cursoropake ZeichenfolgekeinerAus einer vorigen Seite

Gleicher Umschlag und gleiche Eintragsform wie GET /v1/artists (Veranstalter-Felder statt Künstler-Felder; kein Genre-Filter). Fehler: validation_error (400), invalid_cursor (400).

GET /v1/promoters/{id}

Ein verwalteter, veröffentlichter Veranstalter, volle Darstellung, direkt geliefert.

Scope read:promoters. 300 s zwischengespeichert.

Fehler: not_found (404, fehlerhaft geformte ID oder ein Veranstalter, den die Crew nicht verwaltet).

GET /v1/promoters/{id}/events

Veröffentlichte Veranstaltungen eines verwalteten Veranstalters: der Haupt-Feed, auf dem eine Partner-Website aufbaut.

Scopes read:promoters und read:events (beide nötig). 60 s zwischengespeichert.

ParameterWerteStandardBedeutung
statusupcoming, past, allupcomingWelche Veranstaltungen
from / toISO-8601-Datum oder -ZeitpunktkeinerBeginnend zu oder nach / zu oder vor
expandkommagetrennte Liste aus venue, lineup, ticket_tierskeinerVerknüpfte Ressourcen einbetten
limitGanzzahl, 1 bis 10025Seitengröße
cursoropake ZeichenfolgekeinerAus einer vorigen Seite
{
  "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 ist standardmäßig eine leichte Referenz { id, name }; expand=venue hebt sie zum vollen Objekt der öffentlichen Stufe an, und expand=lineup / expand=ticket_tiers fügen jedem Eintrag diese Arrays hinzu (gleiche Form wie bei GET /v1/events/{id}). Fehler: validation_error (400, fehlerhaftes status, from, to oder expand), invalid_cursor (400), not_found (404, fehlerhaft geformte ID oder ein Veranstalter, den die Crew nicht verwaltet).

GET /v1/promoters/{id}/ics

Die kommenden Veranstaltungen des Veranstalters als iCalendar-Feed, gleiche Form wie GET /v1/artists/{id}/ics (siehe Künstler), heruntergeladen als promoter-{id}.ics.

Scopes read:promoters und read:events (beide nötig). 300 s zwischengespeichert.

Fehler: not_found (404, fehlerhaft geformte ID oder ein Veranstalter, den die Crew nicht verwaltet).

GET /v1/promoters/{id}/venues

Die Veranstaltungsorte, an denen die veröffentlichten Veranstaltungen des Veranstalters stattfinden, meistgenutzte zuerst.

Scopes read:promoters und read:venues (beide nötig). 300 s zwischengespeichert.

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

Fehler: not_found (404, fehlerhaft geformte ID oder ein Veranstalter, den die Crew nicht verwaltet).

GET /v1/promoters/{id}/artists

Die Künstler, die der Veranstalter auf seinen veröffentlichten Veranstaltungen gebucht hat, meistgebuchte zuerst. Roster-Künstler kommen in voller Stufe, die übrigen in öffentlicher Stufe.

Scopes read:promoters und read:artists (beide nötig). 300 s zwischengespeichert.

{
  "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 zählt Veranstaltungen, keine Sets: eine Doppelbuchung am selben Abend zählt trotzdem nur einmal. Fehler: not_found (404, fehlerhaft geformte ID oder ein Veranstalter, den die Crew nicht verwaltet).


Veranstaltungen

Set-Zeiten tragen den eigenen Versatz der Veranstaltung.start_time und end_time an einem Lineup-Eintrag, und jede Zeit innerhalb von GET /v1/events/{id}/days und GET /v1/events/{id}/timetable, bleiben als RFC 3339 mit dem eigenen UTC-Versatz der Veranstaltung, zum Beispiel 22:00:00+02:00 statt 22:00:00+00:00: Die Ziffern sind genau das, was der Organisator eingetippt hat, und der Zeitpunkt ist der richtige. Sie sind null, solange der Organisator den Timetable ausgeschaltet lässt (event-days.ts, apiSetTime).GET /v1/events/{id}/days folgt den Tagen, die der Organisator in den Festivaltage-Einstellungen erklärt hat, oder behandelt die ganze Veranstaltung als einen Tag, wenn keine erklärt sind; unscheduled hält Sets ohne Zeit, oder außerhalb jedes erklärten Tages. GET /v1/events/{id}/timetable folgt der eigenen Stage-Reihenfolge des Organisators, nicht erklärte Stages danach alphabetisch, und listet Sets ohne Stage zuletzt.

Jede Route unten akzeptiert einen veröffentlichbaren Schlüssel.

GET /v1/events

Der vereinigte Feed des Crew-Graphen: die Veranstaltungen ihrer Veranstalter, die bestätigten Gigs ihres Rosters, und die Agenda der Veranstaltungsorte, die sie verwaltet. Jeder Eintrag ist eine leichte Zusammenfassung mit einer venue-Referenz.

Scope read:events. 60 s zwischengespeichert.

ParameterWerteStandardBedeutung
statusupcoming, past, allupcomingWelche Veranstaltungen
from / toISO-8601-Datum oder -ZeitpunktkeinerDatumsfenster
promoter_idpositive GanzzahlkeinerEin verwalteter Veranstalter
artist_idpositive GanzzahlkeinerEin Roster-Künstler
venue_idpositive GanzzahlkeinerEin Veranstaltungsort des Graphen
genrekommagetrennte Liste von Genre-IDs, höchstens 10keinerJedes dieser Genres
cityText, 1 bis 100 ZeichenkeinerExakter Stadtname
countryISO 3166-1 alpha-2keinerLändercode
nearlat,lng in DezimalgradkeinerMittelpunkt einer Umkreissuche
radius_kmZahl, 1 bis 50025Umkreis um near (braucht near)
updated_sinceISO-8601-Datum oder -ZeitpunktkeinerGeändert zu oder nach diesem Zeitpunkt
has_ticketstrue, falsekeinerOb die Veranstaltung einen Tickettyp hat
idskommagetrennte Liste von IDs, höchstens 50keinerNur diese Veranstaltungen
limitGanzzahl, 1 bis 10025Seitengröße
cursoropake ZeichenfolgekeinerAus einer vorigen Seite

Dieser Feed unterstützt expand nicht: Es mitzuschicken liefert validation_error (400). Nutz GET /v1/events/{id} oder GET /v1/promoters/{id}/events für erweiterte Daten.

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

Fehler: validation_error (400, expand mitgeschickt, fehlerhafter Filterwert, oder radius_km ohne near), invalid_cursor (400), not_found (404, promoter_id oder artist_id nennt eine Seite, die die Crew nicht verwaltet).

GET /v1/events/search

Derselbe Feed wie GET /v1/events, gefiltert auf Veranstaltungen, deren Titel, Beschreibung oder bestätigtes Lineup den Suchtext enthält.

Scope read:events. 60 s zwischengespeichert.

Jeder Filter von GET /v1/events, dazu:

ParameterWerteStandardBedeutung
qText, 2 bis 100 ZeichenPflichtAls Text gegen Titel, Beschreibung und Lineup abgeglichen

Gleicher Umschlag wie GET /v1/events. Fehler: validation_error (400, q fehlt oder hat die falsche Länge, oder einer der gemeinsamen Filter), invalid_cursor (400), not_found (404, promoter_id oder artist_id außerhalb des Graphen).

GET /v1/events/{id}

Eine veröffentlichte Veranstaltung des Crew-Graphen, direkt geliefert. venue (volle öffentliche Stufe) und lineup sind immer eingebettet; genres ist immer vorhanden.

Scope read:events. 60 s zwischengespeichert.

ParameterWerteStandardBedeutung
expandticket_tierskeinervenue und lineup sind schon eingebettet, das ist also der einzige Wert, der etwas hinzufügt

Wenn expand=ticket_tiers gesetzt ist, trägt die Antwort außerdem ein Objekt ticket_fee: { "percent": number, "fixed": number, "applies_to": "ticket" | "order" }, die Servicegebühr für den Zahlungsempfänger dieser Veranstaltung, in EUR. Es ist dasselbe Objekt wie bei GET /v1/events/{id}/fees, das erklärt, wie sie anzuwenden ist.

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

Lineup-Einträge tragen das eigene DTO des Künstlers plus stage, start_time, end_time und artist_id: Roster-Künstler kommen in voller Stufe (mit contact_emails), fremde Gäste in öffentlicher Stufe. Ein eigener, nicht verknüpfter Act erscheint als Platzhalter-Eintrag mit artist_id: null und seinem eingetippten Namen unter name. Der zweite Lineup-Eintrag oben hat start_time/end_time: null, weil für dieses Set keine Zeit hinterlegt ist, nicht weil der Timetable aus ist; siehe die Anmerkung über dem Abschnitt.

Fehler: validation_error (400, unbekannter Wert für expand), not_found (404, fehlerhaft geformte ID, unveröffentlichte Veranstaltung, oder eine Veranstaltung außerhalb des Graphen der Crew).

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

Die Tickettypen der Veranstaltung: Name, Beschreibung, Preis, Status, Verkaufsfenster und ein Deep Link checkout_url zum Sway-Checkout. Keine Bestandszahlen, keine Bestellungen, keine Käuferdaten. Eine kleine, feste Liste: verpackt in { "data": [...] }, aber ohne pagination und ohne Cursor.

Scope read:events. 30 s zwischengespeichert.

{
  "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 wird serverseitig abgeleitet: on_sale, scheduled, sold_out oder off_sale. currency ist standardmäßig EUR. Fehler: not_found (404).

fee_amount ist die Servicegebühr auf ein Ticket und price_with_fee der Gesamtpreis, den der Käufer zahlt (price plus fee_amount), nach dem Gebührenmodell des ausgezahlten Veranstalters, dieselben Zahlen wie auf der Veranstaltungsseite und im öffentlichen MCP. Zeige zuerst price_with_fee und fee_amount als Servicegebühr. Beim alten Modell pro Bestellung (fee_applies_to: "order") sind beide null: Die Gebühr hängt vom ganzen Warenkorb ab, siehe GET /v1/events/{id}/fees. Kostenlose Tickettypen haben ein fee_amount von 0.

GET /v1/events/{id}/availability

Ob gerade gekauft werden kann: im Verkauf, geplant, ausverkauft oder vorbei, pro Tickettyp und für die Veranstaltung als Ganzes, mit einem Flag "wenige übrig". Nie eine Bestandszahl.

Scope read:events. 30 s zwischengespeichert.

{
  "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 wird gesetzt, sobald der verbleibende Bestand eines Tickettyps bei oder unter einem Zehntel seines Anfangsbestands liegt (mindestens 5), und nur solange er on_sale ist. Der eigene status der Veranstaltung kann no_tickets sein, wenn die Veranstaltung überhaupt keinen Tickettyp hat. Fehler: not_found (404).

GET /v1/events/{id}/fees

Die Servicegebühr, die ein Käufer auf die Tickets dieser Veranstaltung zahlt, aus dem Gebührenmodell des empfangenden Veranstalters: standardmäßig ein Prozentsatz des Preises jedes bezahlten Tickets plus ein fester Betrag pro Ticket.

Scope read:events. 300 s zwischengespeichert.

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

Mit applies_to: "ticket" trägt jedes bezahlte Ticket round(price * percent + fixed) auf den Cent gerundet (ein Bundle zählt als ein Ticket, ein kostenloses Ticket trägt nichts), und total = subtotal + die Summe dieser Gebühren. Einige Veranstalter behalten ein älteres Modell, applies_to: "order": total = subtotal + subtotal * percent + fixed, einmal pro bezahlter Bestellung. So oder so entspricht das Ergebnis der Summe, die der Stripe-Checkout belastet. Fällt auf den Standard-Satz pro Ticket zurück, wenn sich der Zahlungsempfänger nicht auflösen lässt, statt zu scheitern. Fehler: not_found (404).

GET /v1/events/{id}/days

Die Tage, die der Organisator in den Festivaltage-Einstellungen der Veranstaltung erklärt hat, jeder mit den Sets, die in ihm beginnen; oder die Veranstaltung als einzelner Tag, wenn keine erklärt sind.

Scope read:events. 60 s zwischengespeichert.

{
  "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 hält Sets, die keine Zeit haben, oder die außerhalb jedes erklärten Tages liegen. Mit ausgeschaltetem Timetable ist timetable_published false, und jeder Tag kommt mit einem leeren Array slots. Ein B2B-Set ist ein Slot mit mehreren artists; name ist der eigene Name des Organisators, wenn gesetzt, sonst die Künstler verbunden mit " B2B ". Fehler: not_found (404).

GET /v1/events/{id}/timetable

Die zeitlich geplanten Sets nach Stage, die Stages in der eigenen erklärten Reihenfolge des Organisators (der Rest alphabetisch, die ohne Stage zuletzt), jede Stage in Ablaufreihenfolge.

Scope read:events. 60 s zwischengespeichert.

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

Ein Set ohne Zeit steht auf keinem Timetable und bleibt ganz außen vor (es kann trotzdem in GET /v1/events/{id}/days, unter unscheduled, erscheinen). Mit ausgeschaltetem Timetable ist published false und stages ist leer. Fehler: not_found (404).

GET /v1/events/{id}/ics

Die Veranstaltung als iCalendar-Feed, gleiche Form wie GET /v1/artists/{id}/ics (siehe Künstler), für einen Button "Zum Kalender hinzufügen". Heruntergeladen als event-{id}.ics.

Scope read:events. 300 s zwischengespeichert.

Fehler: not_found (404, fehlerhaft geformte ID, unveröffentlichte Veranstaltung oder außerhalb des Graphen der Crew, oder die Veranstaltung hat kein Startdatum).

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

Die Veranstaltung als schema.org-JSON-LD (MusicEvent), bereit, um unverändert in das <script type="application/ld+json"> einer Partnerseite für Suchmaschinen eingefügt zu werden.

Scope read:events. 300 s zwischengespeichert.

{
  "@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 wird aus den Sets des Timetables gebaut (nicht aus dem rohen Lineup), listet Namen also je einmal und folgt derselben Regel "Timetable aus" wie die Routen oben. Fehler: not_found (404).

GET /v1/events/{id}/gallery

Die öffentlichen Fotoalben der Veranstaltung, jedes mit den Fotos, deren Upload abgeschlossen ist (die ersten 200), und wie viele es insgesamt enthält.

Scope read:events. 300 s zwischengespeichert.

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

Private Alben und noch hochladende Fotos erscheinen nie. Fehler: not_found (404).

GET /v1/events/{id}/partners

Die Partner der Veranstalter und Veranstaltungsorte der Veranstaltung, in Anzeigereihenfolge: gleiche Form wie GET /v1/partners (siehe Content), verpackt unter { "event_id": 4521, "data": [...] }.

Scopes read:events und read:content (beide nötig). 300 s zwischengespeichert.

Fehler: not_found (404, unveröffentlichte Veranstaltung oder außerhalb des Graphen der Crew).

GET /v1/events/{id}/news

Die veröffentlichten News der Veranstaltung, neueste zuerst, als Markdown und als bereinigtes HTML: gleiche Form wie GET /v1/news (siehe Content), verpackt unter event_id.

Scopes read:events und read:content (beide nötig). 120 s zwischengespeichert.

ParameterWerteStandardBedeutung
limitGanzzahl, 1 bis 10025Seitengröße
cursoropake ZeichenfolgekeinerAus einer vorigen Seite

Fehler: not_found (404, unveröffentlichte Veranstaltung oder außerhalb des Graphen der Crew), invalid_cursor (400).

GET /v1/events/{id}/presales

Die Presales der Veranstaltung, die keine Entwürfe sind, genau wie die öffentliche Presale-Seite sie zeigt: Teaser, Registrierungsfenster, ob die Registrierung gerade offen ist.

Scope read:events. 60 s zwischengespeichert.

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

Die Produkte, Obergrenzen, Auswahlregel, der Veranstalter und die Mailingliste hinter einem Presale werden nie offengelegt. Fehler: not_found (404).


Veranstaltungsorte

Jede Route unten akzeptiert einen veröffentlichbaren Schlüssel.

GET /v1/venues

Die Veranstaltungsorte des Crew-Graphen: die, die sie verwaltet (managed: true), und die, die ihre veröffentlichten Veranstaltungen beherbergen, öffentliche Stufe, nach Namen.

Scope read:venues. 300 s zwischengespeichert.

ParameterWerteStandardBedeutung
qText, 2 bis 100 ZeichenkeinerName enthält
idskommagetrennte Liste von IDs, höchstens 50keinerNur diese Veranstaltungsorte
limitGanzzahl, 1 bis 10025Seitengröße
cursoropake ZeichenfolgekeinerAus einer vorigen Seite
{
  "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 }
}

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

GET /v1/venues/{id}

Ein Veranstaltungsort des Crew-Graphen, öffentliche Stufe, mit seiner Kapazität, direkt geliefert. Nur erreichbar, wenn die Crew ihn verwaltet oder er eine veröffentlichte Veranstaltung des Graphen beherbergt.

Scope read:venues. 300 s zwischengespeichert.

Gleiche Eintragsform wie oben (mit managed). Fehler: not_found (404).

GET /v1/venues/{id}/events

Die Agenda des Veranstaltungsorts: jede veröffentlichte Veranstaltung des Crew-Graphen, die dort spielt (alle, wenn die Crew den Veranstaltungsort verwaltet). Gleiche Filter wie GET /v1/events, ohne die Seiten-/Künstler-/Veranstaltungsort-Filter.

Scopes read:venues und read:events (beide nötig). 60 s zwischengespeichert.

ParameterWerteStandardBedeutung
statusupcoming, past, allupcomingWelche Veranstaltungen
from / toISO-8601-Datum oder -ZeitpunktkeinerDatumsfenster
limitGanzzahl, 1 bis 10025Seitengröße
cursoropake ZeichenfolgekeinerAus einer vorigen Seite

Gleicher Umschlag und gleiche Eintragsform wie GET /v1/events. Fehler: not_found (404, fehlerhaft geformte ID oder Veranstaltungsort außerhalb des Graphen), validation_error (400), invalid_cursor (400).

GET /v1/venues/{id}/capacity

Wie viele Personen der Veranstaltungsort pro Raum-Layout fasst (standing, seated, theatre, cabaret, banquet, cocktail, classroom), und die größte Zahl: Layouts sind alternative Aufbauten desselben Raums, nie Zonen, die sich aufaddieren.

Scope read:venues. 300 s zwischengespeichert.

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

max und layouts sind null und {}, wenn der Veranstaltungsort keine Kapazität angibt. Fehler: not_found (404).


Content

Jede Route unten akzeptiert einen veröffentlichbaren Schlüssel.

GET /v1/news

Die veröffentlichten News einer Seite, die die Crew verwaltet (ein Veranstalter, ein Veranstaltungsort oder ein Künstler), neueste zuerst, als Markdown und als bereinigtes HTML. Ein für später geplanter Beitrag bleibt bis zu seiner Stunde außen vor. GET /v1/events/{id}/news (siehe Veranstaltungen) ist derselbe Feed für eine Veranstaltung statt für eine Seite.

Scope read:content. 120 s zwischengespeichert.

ParameterWerteStandardBedeutung
promoter_id / venue_id / artist_idpositive GanzzahlkeinerGenau einer, nennt eine Seite, die die Crew verwaltet
limitGanzzahl, 1 bis 10025Seitengröße
cursoropake ZeichenfolgekeinerAus einer vorigen Seite
{
  "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 }
}

Fehler: validation_error (400, nicht genau eines von promoter_id / venue_id / artist_id), invalid_cursor (400), not_found (404, die genannte Seite wird nicht von der Crew verwaltet).

GET /v1/partners

Die Partner eines Veranstalters oder Veranstaltungsorts, den die Crew verwaltet, in Anzeigereihenfolge.

Scope read:content. 300 s zwischengespeichert.

ParameterWerteStandardBedeutung
promoter_id / venue_idpositive GanzzahlkeinerGenau einer, nennt eine Seite, die die Crew verwaltet
{
  "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 } }
  ]
}

Fehler: validation_error (400, nicht genau eines von promoter_id / venue_id), not_found (404, die genannte Seite wird nicht von der Crew verwaltet).

GET /v1/presskits

Die Pressekits jedes Künstlers, den die Crew verwaltet, mit ihrer Sichtbarkeit (öffentlich, privat, Entwurf) und ob ein Passwort sie schützt. Ein veröffentlichbarer Schlüssel sieht nur die öffentlichen; ein geheimer Schlüssel sieht alle drei Zustände.

Scope read:content. 120 s zwischengespeichert.

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

Keine routenspezifischen Fehler.

GET /v1/presskits/{id}

Ein Pressekit genau so, wie es seine eigene Seite zeigt: der Künstler, die Abschnitte in der gewählten Reihenfolge, ihre Texte und Dateien. Ein geschütztes Kit will sein Passwort im Header Presskit-Password, von jedem Aufrufer, die verwaltende Crew eingeschlossen.

Scope read:content. Nie zwischengespeichert (die Antwort hängt von einem Header ab, den der Cache nicht sieht).

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 }]
}

Ein privates Kit öffnet sich nur dem geheimen Schlüssel der verwaltenden Crew; ein Entwurf ist nie erreichbar. Fehler: not_found (404, fehlerhaft geformte ID, Kit nicht gefunden, Künstler nicht verwaltet, Kit ist ein Entwurf, oder ein privates Kit mit dem falschen Schlüsseltyp aufgerufen), password_required (401, Passwort fehlt oder ist falsch), rate_limited (429, höchstens 10 Passwortversuche pro Minute und Schlüssel und Kit, und pro Besucheradresse, wenn mit einem veröffentlichbaren Schlüssel aufgerufen).

GET /v1/forms

Die Formulare der Crew und der Seiten, die sie verwaltet, Entwürfe außen vor, jedes mit der Angabe, ob es gerade Antworten annimmt.

Scope read:content. 120 s zwischengespeichert.

{
  "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 ist eines von draft, scheduled, open, full, closed, empty (ein veröffentlichtes Formular ohne Frage). Fehler: keine über die Authentifizierung hinaus.

GET /v1/forms/{slug}

Die Fragen, das Design und die Verfügbarkeit eines Formulars, um es auf der Partner-Site zu rendern, genau wie es die eigene öffentliche Seite täte.

Scope read:content. 60 s zwischengespeichert.

{
  "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 und design sind die Definition des Formulars, so wie der Formular-Baukasten sie speichert, unverändert durchgereicht: Die Antworten, die du an POST /v1/forms/{slug}/submit postest, sind nach den Frage-IDs innerhalb von steps benannt. captures_contact sagt, ob eine im Formular angegebene Adresse in die Kontakte des Organisators eingeht.

Fehler: not_found (404, fehlerhafte Slug-Form, Entwurfs-Formular, oder ein Formular, das nicht der Crew oder einer ihrer Seiten gehört).


Export

Keine dieser Routen akzeptiert einen veröffentlichbaren Schlüssel; jede braucht read:export plus den passenden Lese-Scope. Nie zwischengespeichert (no-store), und jede ist auf einen Export pro Ressource und Schlüssel alle fünf Minuten gedrosselt, danach lautet die Antwort rate_limited (429). Jede Datei ist auf 10.000 Zeilen gedeckelt und sagt mit einem Header X-Export-Truncated: true|false, ob sie beschnitten wurde.

GET /v1/export/events

GET /v1/export/artists

GET /v1/export/promoters

GET /v1/export/venues

Der veröffentlichte Katalog der Crew, eine Ressource zur Zeit, als newline-delimited JSON (application/x-ndjson): ein JSON-Objekt pro Zeile, kein Umschlag, keine Paginierung. Jede Zeile ist genau das DTO, das die passende Listen-Route ausgibt (GET /v1/events, GET /v1/artists, GET /v1/promoters, GET /v1/venues), ein Export zeigt also nie mehr, als die API ohnehin zeigt.

Scopes read:export + jeweils read:events / read:artists / read:promoters / read:venues.

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

Die Antwort wird heruntergeladen als sway-events.ndjson, sway-artists.ndjson, sway-promoters.ndjson oder sway-venues.ndjson (Content-Disposition: attachment). Fehler: rate_limited (429, ein Export dieser Ressource lief für diesen Schlüssel schon in den letzten fünf Minuten).


Booking-Anfragen

POST /v1/booking-requests

Reicht eine Booking-Anfrage von einer Partner-Site in die Booking-Pipeline der Crew ein und benachrichtigt die Crew. Jeder angefragte Künstler muss ein verwalteter Roster-Künstler sein.

Scope write:bookings (Opt-in). Veröffentlichbare Schlüssel: nein. Nie zwischengespeichert. Nimmt Idempotency-Key an. Enger begrenzt als Lese-Routen: 10 Anfragen pro Minute und Besucher (client_ip), und 300 pro Tag und Crew.

FeldTypPflichtHinweise
artist_idsnumber[]jaNicht leer, höchstens 50 IDs, jede ein verwalteter Roster-Künstler
contact_emailstringjaGültige E-Mail, höchstens 320 Zeichen
contact_namestringneinHöchstens 200 Zeichen
contact_phonestringneinHöchstens 200 Zeichen
event_namestringneinHöchstens 200 Zeichen
event_datestringneinISO-Datum, YYYY-MM-DD
venuestringneinHöchstens 200 Zeichen
citystringneinHöchstens 200 Zeichen
fee_offerstringneinFreitext, höchstens 200 Zeichen
capacitystringneinFreitext, höchstens 200 Zeichen
ticket_pricestringneinFreitext, höchstens 200 Zeichen
organizationstringneinHöchstens 200 Zeichen
{
  "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"
}

Antwort 201 Created, direkt geliefert:

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

Fehler: validation_error (422, fehlerhafte Body-Form, oder artist_ids nennt eine ID außerhalb des Rosters, aufgeführt unter errors[].invalid_ids), rate_limited (429, Limit pro Besucher oder pro Crew und Tag).


Zielgruppe

Keine dieser Routen akzeptiert einen veröffentlichbaren Schlüssel.

POST /v1/newsletter-signup

Fügt eine E-Mail-Adresse dem Newsletter eines verwalteten Veranstalters als CRM-Kontakt hinzu, mit einem DSGVO-Einwilligungseintrag.

Scope write:newsletter (Opt-in). Nie zwischengespeichert. Nimmt Idempotency-Key an. Begrenzt auf 10 Anfragen pro Minute und Besucher (client_ip), und 500 Anmeldungen pro Tag und Veranstalter.

FeldTypPflichtHinweise
emailstringjaGültige E-Mail, höchstens 320 Zeichen
promoter_idnumberjaEin Veranstalter, den die Crew verwaltet
first_name / last_namestringneinHöchstens 200 Zeichen
localestringneinHöchstens 10 Zeichen
countrystringneinHöchstens 2 Zeichen
{ "email": "[email protected]", "promoter_id": 697, "first_name": "Jane" }

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

Fehler: validation_error (422, fehlerhafter Body, oder promoter_id nicht von der Crew verwaltet), rate_limited (429).

POST /v1/newsletter-unsubscribe

Nimmt eine E-Mail-Adresse vom Newsletter eines verwalteten Veranstalters und von dessen Mailinglisten-Anbieter: derselbe CRM-Status, den jede spätere Synchronisierung respektiert.

Scope write:newsletter (Opt-in). Nie zwischengespeichert. Nimmt Idempotency-Key an. Begrenzt auf 10 Anfragen pro Minute und Besucher (client_ip), und 500 pro Tag und Veranstalter.

FeldTypPflichtHinweise
emailstringjaGültige E-Mail
promoter_idnumberjaEin Veranstalter, den die Crew verwaltet

Antwort 200 OK: immer { "status": "unsubscribed" }, ob die Adresse nun auf der Liste stand oder nicht: Die Route darf einem Aufrufer nie verraten, wer abonniert ist.

Fehler: validation_error (422, fehlerhafter Body, oder promoter_id nicht verwaltet), rate_limited (429).

POST /v1/forms/{slug}/submit

Sendet die Antworten eines Formulars, das die Partner-Site aus GET /v1/forms/{slug} gerendert hat: geprüft gegen die gespeicherte Definition, im CRM des Eigentümers abgelegt, wenn das Formular einen Kontakt erfasst, mit api:form:{slug} als vermerkter Einwilligungsquelle.

Scope write:forms (Opt-in). Nie zwischengespeichert. Nimmt Idempotency-Key an. Begrenzt auf 60 Einreichungen pro Minute und Schlüssel und Formular, 5 pro Minute und Besucheradresse (wenn client_ip mitgeschickt wird), und 5.000 pro Tag und Formular.

FeldTypPflichtHinweise
answersobjectjaBenannt nach der Feld-ID, wie die eigene Definition des Formulars sie nennt
localestringneinZweibuchstabiges Sprachkürzel, optional mit Region
honeypotstringneinEin verstecktes Feld im eigenen Formular des Partners: jeder Text darin lässt die Antwort still fallen
client_ipstringneinDie Adresse des Besuchers, nur genutzt, um seine Einreichungen zu zählen
{ "answers": { "email": "[email protected]", "s1_q1": "Yes" } }

Antwort 201 Created: { "status": "received", "redirect_url": null } (ein ausgefülltes Honeypot-Feld antwortet genauso, speichert aber nichts).

Fehler: not_found (404, fehlerhafter Slug, Entwurfs-Formular, oder ein Formular, das nicht der Crew gehört), validation_error (422, fehlerhafter Body oder Antworten, die an der eigenen Prüfung des Formulars scheitern, unter errors[].param als answers.<field>), conflict (409, not_taking_answers, wenn das Formular gerade nicht offen ist, already_answered, wenn das Formular nur eine Antwort pro Adresse erlaubt und diese schon geantwortet hat), rate_limited (429).


Checkout

Keine dieser Routen akzeptiert einen veröffentlichbaren Schlüssel.

POST /v1/checkout-sessions

Legt eine gehostete Stripe-Checkout-Session für einen öffentlichen Shop an und liefert ihre url. Der Käufer wird dorthin weitergeleitet und kehrt danach auf Sways eigene Seite /success oder /cancel zurück, nie auf die Site des Aufrufers; die Abwicklung (Bestellung, Tickets, Bestätigungs-E-Mail) übernimmt vollständig Sway.

Scope write:checkout (Opt-in); ein Warenkorb mit einer Summe von exakt null braucht zusätzlich write:checkout:free. Nie zwischengespeichert. Nimmt keinen Idempotency-Key an: Diese Route hat stattdessen ihre eigene Absicherung (idempotency_key im Body, unten). Begrenzt auf 60 Anfragen pro Minute und Käufer (client_ip), 600 pro Minute und Schlüssel, 5.000 Checkouts pro Tag und Crew, und 30 bezahlte (oder 10 kostenlose) Checkouts pro Tag und Käufer-E-Mail.

Alles, was für Geld und Bestand maßgeblich ist, wird serverseitig bestimmt und kann vom Aufrufer nie geliefert oder überschrieben werden: Preis, Währung, das Stripe-Konto des Zahlungsempfängers, die Servicegebühr und die Bestandsreservierung. Die Crew des Schlüssels muss den Veranstalter verwalten, der die Gelder empfängt, sonst validation_error.

FeldTypPflichtHinweise
event_idnumberjaMuss veröffentlicht sein und im Graphen der Crew liegen
line_itemsarrayja1 bis 20 Einträge { "tier_id": string, "quantity": number }; tier_id ist eine Tickettyp-ID aus GET /v1/events/{id}/ticket-tiers, quantity 1 bis 50
buyer_emailstringjaGültige E-Mail, höchstens 320 Zeichen: wohin die Tickets geliefert werden
idempotency_keystringneinEine UUID pro Kaufklick; ein erneuter Versuch mit demselben Wert liefert dieselbe Session
discount_idstringneinEine Gutschein-ID für die Veranstaltung, aus POST /v1/coupons/validate
access_codestringneinEin Presale- oder Zugangscode, der einen versteckten Tickettyp freischaltet
referencestringneinFreitext, höchstens 64 Zeichen aus [A-Za-z0-9._:-], nur an Analytics zurückgespiegelt
localestringneinDie Sprache des Käufers (en, fr, nl...), mit der die Servicegebühr-Zeile auf der Stripe-Zahlungsseite benannt wird. Standard ist der Accept-Language der Anfrage, dann Englisch
marketing_consentbooleanneinWird auf diesem Weg ignoriert; eine von einem API-Schlüssel behauptete Einwilligung wird für die CRM-Aufnahme nie vertraut
consentobjectneinBooleans { analytics, marketing } plus version/at, nur für die Anzeigenzuordnung weitergereicht
client_ip / client_user_agent / event_source_urlstringneinKäuferseitige Signale für den Abgleich der Anzeigenzuordnung, und client_ip, um die Checkouts des Käufers zu zählen. Nie für etwas anderes genutzt

Felder, die mit validation_error abgelehnt werden, wenn mitgeschickt, weil jedes serverseitig bestimmt wird: amount / price / unit_amount, currency, jeder Zahlungsempfänger oder 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"
}

Antwort 200 OK, direkt geliefert:

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

checkout_id folgt nur bei einer bezahlten Session; eine kostenlose (Frei-)Bestellung liefert nur { "url": ... }, da es danach keine Stripe-Session zu verfolgen gibt.

Ein bezahlter Checkout hält seine Tickets sowie eine Nutzung seines Gutscheins und seines Zugangscodes für die 30 Minuten der Session zurück. Was der Käufer nicht bezahlt, kommt zurück, wenn die Session abläuft; bis dahin antwortet ein Code, dessen letzte Nutzung zurückgehalten ist, mit validation_error.

Kostenlose Bestellungen (null Euro). Ein Warenkorb mit einer Summe von null, durch einen 100-%-Gutschein oder einen wirklich kostenlosen Tickettyp, ist eine eigene Fähigkeit mit höherem Vertrauen: Der Schlüssel braucht zusätzlich write:checkout:free. Kostenlose und bezahlte Tickets lassen sich nicht in einer Bestellung kombinieren.

Fehler: missing_scope (403, kein write:checkout, oder eine kostenlose Bestellung ohne write:checkout:free), not_found (404, unveröffentlichte Veranstaltung oder außerhalb des Graphen), validation_error (422, fehlerhafter Body, unbekannter oder nicht verfügbarer Tickettyp, Menge über max_per_order, gemischte kostenlose und bezahlte Positionen, oder der Zahlungsempfänger nicht von der Crew verwaltet), conflict (409, das Zahlungskonto des Veranstalters ist nicht bereit), rate_limited (429, sieh Retry-After), upstream_error (502, Stripe konnte die Session nicht anlegen).

GET /v1/checkout-sessions/{id}

Wo ein Checkout steht, den diese Crew geöffnet hat, adressiert über die checkout_id, die seine Erstellung zurückgab, nie die Stripe-Session-ID: offen (mit seinem Zahlungslink), abgeschlossen oder abgelaufen.

Scope write:checkout. Nie zwischengespeichert (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 ist nur vorhanden, solange status open ist. Fehler: not_found (404, fehlerhaft geformtes Handle, oder ein Checkout, der für eine andere Crew versiegelt ist).

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

Beendet einen offenen Checkout, den diese Crew geöffnet hat, und gibt seine Tickets sofort an den Bestand zurück, statt auf Stripes eigenen Timer zu warten. Idempotent: Es erneut auf einem schon abgelaufenen Checkout aufzurufen, antwortet einfach mit seinem aktuellen Zustand.

Scope write:checkout. Nie zwischengespeichert. Nimmt Idempotency-Key an.

Gleiche Antwortform wie GET /v1/checkout-sessions/{id}. Fehler: not_found (404, wie oben), conflict (409, der Checkout war schon bezahlt: eine Erstattung läuft stattdessen über den Organisator).

POST /v1/coupons/validate

Prüft einen Aktionscode vor dem Checkout: die weiterzugebende discount_id, ihre Bedingungen, und die Summen des Warenkorbs, wenn einer mitgeschickt wird, oder warum er nicht gilt. Antwortet so oder so mit 200: ein abgelehnter Code ist eine Antwort, kein Fehler.

Scope write:checkout. Nie zwischengespeichert. Nimmt keinen Idempotency-Key an (writeClass: check): Es läuft jedes Mal, bis zu 300 Prüfungen pro Minute und Schlüssel, zusätzlich zu den unten beschriebenen Grenzen gegen Erraten.

FeldTypPflichtHinweise
event_idnumberjaMuss eine Veranstaltung sein, deren Zahlungsempfänger die Crew verwaltet
codestringja1 bis 64 druckbare Zeichen
line_itemsarrayneinDer Warenkorb des Checkouts, gleiche Form wie bei POST /v1/checkout-sessions
client_ipstringneinDie Adresse des Käufers, nur genutzt, um seine Fehlversuche zu zählen
{ "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 }
}

Ein Code, der zu einer anderen Veranstaltung, einem anderen Organisator gehört, oder ausgeschaltet ist, antwortet ununterscheidbar mit { "valid": false, "code": "...", "reason": "not_found" }. Andere Werte für reason: min_order (mit min_order_amount), product_limit (mit products_remaining), expired, usage_limit, not_open_here, not_applicable. Die Stripe-Gutschein-ID verlässt das System nie.

Fehler: validation_error (422, fehlerhafter Body), not_found (404, unveröffentlichte Veranstaltung oder außerhalb des Graphen), validation_error (422, Zahlungsempfänger nicht von der Crew verwaltet), rate_limited (429, die Prüfungs-Obergrenze pro Minute, oder die Grenze gegen Erraten unten).

POST /v1/access-codes/validate

Prüft einen Presale- oder Zugangscode: die versteckten Tickettypen, die er freischaltet (nie von GET /v1/events/{id}/ticket-tiers gelistet), ob der Organisator will, dass nur diese Tickettypen gezeigt werden, und die eigene Obergrenze des Codes pro Bestellung, oder warum er nicht funktioniert.

Scope write:checkout. Nie zwischengespeichert. Nimmt keinen Idempotency-Key an (writeClass: check), gleiche Obergrenze von 300 Prüfungen pro Minute wie bei Gutscheinen.

FeldTypPflichtHinweise
event_idnumberjaMuss eine Veranstaltung sein, deren Zahlungsempfänger die Crew verwaltet
codestringja1 bis 64 druckbare Zeichen
client_ipstringneinDie Adresse des Käufers, nur genutzt, um seine Fehlversuche zu zählen
{
  "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 bedeutet, dass der Organisator will, dass nur diese freigeschalteten Tickettypen gezeigt werden, sobald der Code eingegeben ist. Die eigenen internen IDs des Codes verlassen das System nie. Andere Werte für reason bei Ablehnung: not_found, inactive, not_started, ended, usage_limit.

Grenzen gegen Erraten (beide Routen oben). Über die allgemeine Prüfungs-Obergrenze pro Minute hinaus wird ein unbekannter Code pro Schlüssel und Veranstaltung gezählt (300 Fehlversuche pro 10 Minuten) und, wenn der Aufrufer client_ip mitschickt, auch pro Käuferadresse (10 Fehlversuche pro 15 Minuten): Jenseits einer der beiden Grenzen lautet die Antwort rate_limited (429), noch bevor der Code überhaupt nachgeschlagen wird.

Fehler: dieselben wie bei POST /v1/coupons/validate.


Shop

Die drei Lese-Routen akzeptieren einen veröffentlichbaren Schlüssel; Angebot und Checkout nicht.

Das Merch der Seiten, die deine Crew verwaltet, für einen eigenen Shop: der Katalog einer Seite, ein Produkt, was ein Event verkauft, der Preis eines Warenkorbs und ein Stripe-Checkout auf dem eigenen Konto der verkaufenden Seite. Der Käufer zahlt bei Stripe und kommt auf Sways Seiten zurück; Sway legt die Bestellung an, schreibt Käufer und Verkäufer, und der Verkäufer erledigt Versand, Widerrufe und Erstattungen im Bildschirm Shop der Seite.

Eine Seite verkauft, sobald ihr Shop geöffnet ist; vorher liest sich ihr Katalog leer. Preise enthalten die MwSt. und stehen in Euro-Cent. Keine Route nennt einen Bestand: eine Variante ist available oder nicht.

GET /v1/shop/products

Das veröffentlichte Merch einer Seite der Crew, mit der Verkäuferidentität, die Käufer vor dem Bezahlen sehen müssen. Event-Exklusives sind nicht dabei: lies sie beim Event.

Scope read:shop. Veröffentlichbare Schlüssel: ja. 30 s zwischengespeichert.

ParameterTypPflichtHinweise
seller_typestringjapromoter, artist oder venue
seller_idnumberjaEine Seite, die die Crew verwaltet
{
  "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 }
    }
  ]
}

Eine kleine Liste: in { "data": [...] } verpackt, ohne pagination. Eine Seite, die die Crew nicht verwaltet oder deren Shop nicht geöffnet ist, antwortet { "seller": null, "data": [] }. fulfilment_modes nennt delivery, pickup_event und pickup_location; country_rule ist all, only oder except und gilt für countries. Fehler: validation_error (422, seller_type oder seller_id fehlt oder ist ungültig).

preorder ist null oder { "status": "open", "ships_on": "2026-11-15", "ends_on": null } für ein Produkt, das vor seiner Fertigstellung verkauft wird: Versand ab ships_on, Vorbestellungen bis ends_on, wenn gesetzt. status ist closed, sobald die Vorbestellung vor dem Versandtag endete; die Varianten melden dann available: false. Am Versandtag verkauft sich das Produkt wie jedes andere, und preorder wird wieder null.

installments ist null oder das Ratenzahlungsangebot von Stripe auf dem Konto der verkaufenden Seite. Ist es gesetzt, zeige das Payment Method Messaging Element von Stripe mit publishable_key und stripe_account (Stripe.js), nie eigene Worte: Werbung für Ratenkredite ist geregelt. methods nennt, was der Checkout anbietet (klarna); country ist das des Verkäufers, ein Standardwert, bis der Käufer ein Lieferland wählt. Das Produkt und der Event-Shop tragen dasselbe Feld.

GET /v1/shop/products/{id}

Ein Produkt für seine Produktseite, direkt zurückgegeben, mit seller als vollständiger Verkäuferidentität. Mit event_id das Produkt so, wie dieses Event es verkauft, Exklusives inklusive.

Scope read:shop. Veröffentlichbare Schlüssel: ja. 30 s zwischengespeichert.

ParameterTypPflichtHinweise
event_idnumberneinEin Event aus dem Graph der Crew, dessen Shop geöffnet ist

Fehler: not_found (404, unbekanntes oder unveröffentlichtes Produkt, eine Seite, die die Crew nicht verwaltet, ein Event-Exklusiv ohne sein Event, oder ein Event mit geschlossenem Shop).

GET /v1/events/{id}/shop

Was ein Event aus dem Graph der Crew verkauft: die Produkte seines bezahlten Veranstalters, alle oder eine Auswahl, Exklusives inklusive, solange der Event-Shop geöffnet ist; und ob Käufer noch die Abholung beim Event wählen können.

Scope read:shop. Veröffentlichbare Schlüssel: ja. 30 s zwischengespeichert.

{
  "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 enthält die vollständige Verkäuferidentität wie in der Liste oben. Die Liste ist leer, mit seller und pickup_at_event auf null, solange der Event-Shop geschlossen ist oder wenn der bezahlte Veranstalter keine Seite der Crew ist, weil der Checkout nicht für ihn verkaufen könnte. Fehler: not_found (404, unveröffentlichtes Event oder eines außerhalb des Graphs).

POST /v1/shop/quote

Berechnet einen Warenkorb vor dem Checkout: die Zeilen nach dem Gutschein, die Versandoptionen für einen Modus und ein Land, die Modi und Länder, die der Warenkorb erlaubt, und die Summe. Nichts wird reserviert.

Scope read:shop. Veröffentlichbare Schlüssel: nein. Nie zwischengespeichert. Begrenzt auf 120 Anfragen pro Minute und Käufer (client_ip).

FeldTypPflichtHinweise
linesarrayja1 bis 20 Einträge { "variant_id": string, "quantity": number }, Menge 1 bis 20
modestringjadelivery, pickup_event oder pickup_location
event_idnumberneinDas Event, aus dem der Warenkorb stammt: es schaltet seine Exklusives und die Abholung beim Event frei
countrystringbei deliveryZweibuchstabiger Code des Lieferlands
coupon_codestringneinEin Code der verkaufenden Seite
method_keystringneinDie gewählte Versandoption aus shipping_options
client_ipstringneinDie Adresse des Käufers, um seine Anfragen zu zählen
{
  "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 ist der Versandtag der Bestellung, wenn der Warenkorb eine Vorbestellung enthält, der späteste davon; der Käufer sieht ihn vor der Zahlung, auch auf der Stripe-Seite. Für einen Warenkorb mit lieferbaren Artikeln ist er null. not_ready_for_event lehnt die Abholung beim Event für einen Warenkorb ab, der erst danach versendet wird; preorder_closed trägt die variant_id, deren Vorbestellung beendet ist.

Fehler: validation_error (422) mit einem 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, oder ein Grund, der mit coupon_ beginnt. max_per_order trägt variant_id und max; eine Versandablehnung trägt available_modes und deliverable_countries. Dazu conflict (409, Grund not_ready: die Seite kann noch nicht verkaufen), not_found (404, ein Event außerhalb des Graphs) und rate_limited (429).

POST /v1/shop/cart-check

Sagt, welche Zeilen des Warenkorbs, den ein Käufer behält, auf ein Produkt zeigen, das den Shop verlassen hat: zurückgezogen, archiviert oder gelöscht, oder eine deaktivierte oder entfernte Variante. Entferne diese Zeilen aus dem Warenkorb. Eine ausverkaufte Variante, eine Seite, die noch nicht verkaufen kann, oder ein Event, dessen Shop geschlossen ist, behält ihre Zeile: Das kann sich wieder ändern, und das Angebot erklärt es. Eine Zeile einer Seite, die deine Crew nicht verwaltet, kommt ebenfalls zurück.

Scope read:shop. Veröffentlichbare Schlüssel: nein. Nie zwischengespeichert. Begrenzt auf 120 Anfragen pro Minute und Käufer (client_ip).

FeldTypPflichtHinweise
linesarrayja1 bis 20 Einträge { "variant_id": string, "product_id": string }
client_ipstringneinDie Adresse des Käufers, um seine Anfragen zu zählen
{ "gone": ["9a1b7c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d"] }

Prüfe den Warenkorb, wenn der Käufer auf die Seite zurückkommt und wenn sich der Warenkorb öffnet.

Fehler: validation_error (422, eine fehlende oder fehlerhafte Zeile) und rate_limited (429).

POST /v1/shop/checkout-sessions

Öffnet einen gehosteten Stripe-Checkout für Merch einer Seite, die die Crew verwaltet, auf dem eigenen Stripe-Konto dieser Seite, und liefert seine url. Der Käufer zahlt dort und landet danach auf Sways Danke-Seite und seiner Bestellseite oder auf /cancel, nie auf der Site des Aufrufers. Sway legt die Bestellung an, schreibt Käufer und Verkäufer und nimmt seine Provision beim Verkäufer.

Scope write:shop:checkout (Opt-in). Veröffentlichbare Schlüssel: nein. Nie zwischengespeichert. Nimmt keinen Idempotency-Key an: schick idempotency_key im Body. Begrenzt auf 60 Anfragen pro Minute und Käufer (client_ip), 5000 Checkouts pro Tag und Crew und 30 pro Tag und Käufer-E-Mail.

Der Body ist der des Angebots, dazu:

FeldTypPflichtHinweise
buyer_emailstringjaWohin Bestätigung und Bestelllink gehen
idempotency_keystringneinEine UUID pro Klick auf Bezahlen; ein erneuter Versuch mit demselben Wert liefert dieselbe Session
localestringneinDie Sprache des Käufers für Stripe und die E-Mails: en, fr, de, es, it, nl oder uk
referencestringneinFrei, höchstens 64 Zeichen aus [A-Za-z0-9._:-], an der Bestellung gespeichert

Felder, die mit validation_error abgelehnt werden, weil jedes serverseitig bestimmt wird: 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"
}

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

Der Bestand wird für die 30 Minuten der Session gehalten; was der Käufer nicht bezahlt, kommt zurück, wenn die Session abläuft. Eine vom Aufrufer gesendete Marketing-Einwilligung wird ignoriert: ein API-Schlüssel trägt nie einen Käufer in ein CRM ein.

Fehler: die des Angebots, dazu validation_error für eine ungültige buyer_email oder reference oder für Produkte einer Seite, die die Crew nicht verwaltet; conflict (409, not_ready, oder der Bestand hat sich unter dem Warenkorb geändert); rate_limited (429, siehe Retry-After); upstream_error (502, Stripe konnte die Session nicht anlegen).


Ambassadors

Das Empfehlungsprogramm des Veranstalters, gerendert auf der eigenen Site des Partners statt auf sway.events. Keine dieser Routen akzeptiert einen veröffentlichbaren Schlüssel: Führ sie von einem Server aus, nie aus einem Browser. Drei Zugangsdaten sind im Spiel: der API-Schlüssel sway_sk_... (welche Crew aufruft), eine Mitglieds-Session amb_st_..., die im Header X-Ambassador-Session mitreist (welcher Ambassador angemeldet ist, geprägt bei Registrierung oder Anmeldung, 8 Stunden), und das eigene First-Party-Cookie des Partners, das einen Besucher dieser Session zuordnet.

Registrierung und Anmeldung sind die eigenen Formulare des Partners; Sway rendert hier keine Seite und sieht kein Widget, der Partner betreibt also seinen eigenen Bot-Schutz (Turnstile, hCaptcha, ein Honeypot). Sways eigener Schutz ist struktureller Art: Die Registrierung ist aus, solange der Veranstalter sie nicht einschaltet, sie verbraucht eine eigene Shop-Einladung mit eigenem Kontingent, und jede Route unten ist gedeckelt pro Schlüssel, pro Besucher und, wo es zählt, pro E-Mail-Adresse.

GET /v1/ambassador-program

Alles, was eine Seite "Werde Ambassador" braucht: der Name und die Beschreibung des Programms, der Rabatt, den es bietet, ob es gerade Leute aufnimmt, und die eigenen Begriffe des Veranstalters für XP, Stufen, Quests und Badges. Nur öffentliche Daten, kein Mitglied und keine Mitgliederzahl.

Scope read:ambassadors. 120 s zwischengespeichert.

ParameterWerteStandardBedeutung
owner_typepromoter, venuePflichtWessen Programm
owner_idpositive GanzzahlPflichtDie ID der Seite
{
  "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 }
}

Die Antwort wiederholt außerdem name, description, signup_open und gamification_enabled auf oberster Ebene, für Sites, die auf der ersten Version des Insomnia-Boilerplates aufbauen. Lies die verschachtelten Felder: Sie sind es, die diese Seite beschreibt.

Ein Eigentümer außerhalb des Graphen der Crew antwortet mit demselben 404 wie einer ohne Programm: Diese Route darf nie verraten, welche Veranstalter, die der Schlüssel nicht verwaltet, zufällig eines betreiben.

Fehler: validation_error (422, owner_type / owner_id fehlt oder ist fehlerhaft geformt: Diese Route ist älter als die 400-Regel für Query-Parameter und behält ihre Antwort), not_found (404).

GET /v1/ambassador-leaderboard

Die Rangliste des Programms, nur wenn der Veranstalter sie veröffentlicht: Rang, Vorname und Anfangsbuchstabe des Nachnamens, Punktzahl. Ein Umsatz-Ranking liefert die Reihenfolge, nie die Beträge.

Scope read:ambassadors. 60 s zwischengespeichert.

ParameterWerteStandardBedeutung
owner_typepromoter, venuePflichtWessen Programm
owner_idpositive GanzzahlPflichtDie ID der Seite
limitGanzzahl, 1 bis 10025Wie viele Ränge
{
  "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 ist tickets, xp oder revenue. Bei revenue ist score null: Die Reihenfolge zeigt, wer am meisten verkauft hat, nie wie viel. tier ist nur gesetzt, solange die Stufen des Programms eingeschaltet sind.

Fehler: validation_error (400, owner_type / owner_id fehlt oder ist fehlerhaft geformt), not_found (404, kein Programm, oder das Programm veröffentlicht seine Rangliste nicht).

GET /v1/ambassador-rewards

Die Prämien, für die Mitglieder ihre Credits ausgeben: Kosten, was übrig ist, wann, und die Stufe, die sie brauchen. Leer, solange Credits ausgeschaltet sind.

Scope read:ambassadors. 60 s zwischengespeichert.

ParameterWerteStandardBedeutung
owner_typepromoter, venuePflichtWessen Programm
owner_idpositive GanzzahlPflichtDie ID der Seite
{
  "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 ist null, wenn die Prämie kein Bestandslimit hat. available ist false außerhalb ihrer Daten oder sobald sie vergriffen ist. claim_prompt ist die Frage, die das Mitglied beim Einlösen beantwortet (das answer von POST /v1/ambassador-claim).

Fehler: validation_error (400, owner_type / owner_id), not_found (404, kein Programm).

GET /v1/ambassador-quests

Die Quests, die Mitglieder gerade annehmen können: Ziel, Daten und Prämien. Der eigene Fortschritt eines angemeldeten Mitglieds steht in GET /v1/ambassador-me, nie hier.

Scope read:ambassadors. 60 s zwischengespeichert.

ParameterWerteStandardBedeutung
owner_typepromoter, venuePflichtWessen Programm
owner_idpositive GanzzahlPflichtDie ID der Seite
{
  "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 ist das, wovon das Mitglied threshold erreichen muss: orders, tickets, revenue_cents, credits_earned, reward_claimed, profile_complete oder manual (der Organisator hakt es ab). scope ist lifetime (seit dem Beitritt des Mitglieds), event (bei einer Veranstaltung, oder bei jeder einzelnen Veranstaltung, wenn event null ist) oder window (zwischen starts_at und ends_at). data ist leer, solange Gamification aus ist.

Fehler: validation_error (400, owner_type / owner_id), not_found (404, kein Programm).

GET /v1/ambassadors

Die Mitglieder des Programms für die eigenen Werkzeuge des Veranstalters, neueste zuerst: Name, E-Mail, Instagram, Code, Status, Verkäufe, Credits, XP und Stufe. Nie das Telefon, das Zugriffstoken oder das Sway-Konto.

Scope read:ambassador-members (nur Eigentümer: nur der Crew-Eigentümer erteilt ihn). Nie zwischengespeichert (no-store).

ParameterWerteStandardBedeutung
owner_typepromoter, venuePflichtWessen Programm
owner_idpositive GanzzahlPflichtDie ID der Seite
qTextkeinerTrifft Name, E-Mail, Instagram oder Code
statusactive, pending, suspendedkeinerNach Status filtern
limitGanzzahl, 1 bis 10025Seitengröße
cursoropake ZeichenfolgekeinerAus einer vorigen Seite
{
  "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 }
}

Fehler: validation_error (400, owner_type / owner_id / status), invalid_cursor (400), not_found (404, kein Programm für diesen Eigentümer).

GET /v1/ambassadors/{id}/ledger

Die Credit-Historie eines Mitglieds, neueste zuerst, mit seinem laufenden Kontostand: was jeder Verkauf, jede Einlösung, jede Quest oder manuelle Vergabe hinzugefügt oder abgezogen hat.

Scope read:ambassador-members. Nie zwischengespeichert.

ParameterWerteStandardBedeutung
limitGanzzahl, 1 bis 10025Seitengröße
cursoropake ZeichenfolgekeinerAus einer vorigen Seite
{
  "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 }
}

Der Eigentümer, den die Crew verwalten muss, wird aus dem eigenen Programm des Mitglieds gelesen, nie aus der Anfrage: Das Mitglied einer anderen Crew antwortet mit demselben 404 wie eine unbekannte ID. Fehler: not_found (404, fehlerhaft geformte ID oder Mitglied außerhalb der Programme der Crew), invalid_cursor (400).

GET /v1/ambassador-stats

Wie es dem Programm geht: Mitglieder nach Status und der Anteil der aktiven, die verkaufen, die Verkäufe, die seine Codes eingebracht haben (lebenslang, und zwischen from und to, wenn angegeben), und die zehn besten Codes über denselben Zeitraum.

Scope read:ambassador-members. Nie zwischengespeichert.

ParameterWerteStandardBedeutung
owner_typepromoter, venuePflichtWessen Programm
owner_idpositive GanzzahlPflichtDie ID der Seite
from / toISO-8601-Datum oder -ZeitpunktkeinerZeitraum, höchstens 400 Tage auseinander, to nach 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": []
}

Fehler: validation_error (400, owner_type / owner_id, oder ein Zeitraum, der umgekehrt oder über 400 Tage lang ist), not_found (404, kein Programm).

POST /v1/ambassador-register

Jemand tritt dem Programm von der eigenen Site des Partners bei. Liefert seinen Code und eine Mitglieds-Session, damit der Shop ihm sofort seine Seite zeigen kann.

Scope write:ambassadors (Opt-in). Nie zwischengespeichert. Nimmt Idempotency-Key an. Begrenzt auf 30 Registrierungen pro Minute und Besucher (client_ip, bemessen für das gemeinsame WLAN eines Veranstaltungsorts), und 300 pro Tag und Programm.

FeldTypPflichtHinweise
owner_typestringjapromoter oder venue
owner_idnumberjaDie ID der Seite
first_name / last_namestringjaHöchstens 120 Zeichen
emailstringjaGültige E-Mail; der einzige Weg zurück zu einer verlorenen Seite
phonestringjaEine echte Telefonnummer; der Dedupe-Schlüssel für das Programm
localestringneinHöchstens 8 Zeichen
{ "owner_type": "promoter", "owner_id": 697, "first_name": "Jane", "last_name": "Doe", "email": "[email protected]", "phone": "+32470000000" }

Antwort: 201 bei einem wirklich neuen Mitglied, 200, wenn es schon existierte:

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

Eine Telefonnummer, die in diesem Programm schon eingeschrieben ist, wird abgelehnt, nicht angemeldet: Wer die Nummer eingetippt hat, ist nicht unbedingt, wem sie gehört.

Fehler: validation_error (422, fehlende oder ungültige Felder, oder eine Wegwerf-/gesperrte E-Mail-Adresse), not_found (404, Eigentümer außerhalb des Graphen der Crew), conflict (409, signup_closed, außer der Veranstalter hat die öffentliche Registrierung eingeschaltet und eine laufende Shop-Einladung hat noch Kontingent, oder already_registered für eine schon eingeschriebene Telefonnummer), rate_limited (429).

POST /v1/ambassador-login

Mailt einem Mitglied einen einmaligen Anmeldelink, der zurück auf die eigene Site des Partners führt.

Scope write:ambassadors. Nie zwischengespeichert. Nimmt Idempotency-Key an. Begrenzt auf 8 pro Minute und Besucher (client_ip), und 5 pro Stunde und E-Mail-Adresse für den eigentlichen Versand.

FeldTypPflichtHinweise
owner_typestringjapromoter oder venue
owner_idnumberjaDie ID der Seite
emailstringjaWohin der Link geschickt wird
return_originstringjaEiner der auf diesem Schlüssel registrierten Origins
return_pathstringneinEin relativer Pfad auf diesem Origin, Standard /ambassador

Antwort 202, immer derselbe Body: { "status": "accepted" }. Eine unbekannte Adresse, ein gesperrtes Mitglied, ein geschlossenes Programm und ein echter Versand sind nicht zu unterscheiden: Ein Anmeldeformular, das bei einer bekannten Adresse anders antwortet, wäre ein Mitgliedschafts-Orakel. return_origin und return_path werden erneut in SQL geprüft, es gibt also keinen Weg, daraus eine offene Weiterleitung zu bauen.

Fehler: validation_error (422, fehlerhafter Body, oder return_origin ist keiner der registrierten Origins des Schlüssels), rate_limited (429, pro Besucher oder pro E-Mail-Adresse).

POST /v1/ambassador-login-exchange

Die Landingpage des Partners empfängt ?amb_token=... und tauscht es serverseitig gegen eine Mitglieds-Session.

Scope write:ambassadors. Nie zwischengespeichert. Nimmt Idempotency-Key an. Begrenzt auf 20 Versuche pro Minute und Besucher (client_ip).

FeldTypPflichtHinweise
tokenstringjaDer Wert amb_lt_... aus dem Query-String
{ "session": { "token": "amb_st_...", "expires_at": "2026-09-01T18:00:00.000Z" }, "return_url": "https://www.example.com/ambassador" }

Einmal nutzbar: Das Token wird von demselben Datenbank-Update verbraucht, das es liest, zwei Klicks in einem Mail-Client, der Links vorab lädt, können also nicht beide gelingen.

Fehler: invalid_key (401, fehlerhaft geformtes, unbekanntes, abgelaufenes oder schon benutztes Token, alle identisch), rate_limited (429).

GET /v1/ambassador-me

Die eigene Seite des angemeldeten Mitglieds: Code, Verkäufe, Credits und Prämien, dazu XP, Stufe, Quests und Badges, wenn der Veranstalter Gamification eingeschaltet hat.

Scope read:ambassadors, plus ein Header X-Ambassador-Session. Nie zwischengespeichert.

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

Dieselbe Mitgliederseite, die sway.events dem Mitglied zeigt, hier gekürzt: game trägt außerdem die eigenen Begriffe des Programms (tier_noun, quest_noun, badge_noun), und next_tier sagt, wie viel XP bis zum Aufstieg fehlt (null an der Spitze). quests, badges, tiers und xp_ledger füllen sich nur, solange Gamification an ist, rewards, claims und ledger (die Credit-Historie) nur, solange Credits an sind. events listet die Veranstaltungen, auf denen der Code des Mitglieds funktioniert, mit seinem Rabatt, und leaderboard die Rangliste, wenn der Veranstalter sie veröffentlicht, mit revenue_cents an jedem Rang nur bei einem Umsatz-Ranking, wie auf sway.events.

access_token (die dauerhafte Zugangsberechtigung des Mitglieds für seine Seite auf sway.events) und seine Telefonnummer erscheinen hier nie, was auch immer die Zeile darunter hält.

Fehler: invalid_key (401, der Header fehlt oder ist fehlerhaft geformt, oder die Session ist abgelaufen oder ungültig).

POST /v1/ambassador-profile

Das angemeldete Mitglied korrigiert seinen eigenen Namen, seine E-Mail oder seine Sprache.

Scope write:ambassadors, plus Session. Nie zwischengespeichert. Nimmt Idempotency-Key an. Begrenzt auf 20 pro Minute und Besucher (client_ip).

FeldTypPflichtHinweise
first_name / last_name / email / localestringneinJede Teilmenge; mindestens eines ist nötig

Die Telefonnummer lässt sich hier nicht ändern (sie ist der Dedupe-Schlüssel: sie zu ändern wäre eine Zusammenführung, keine Bearbeitung), ebenso wenig Status, Code, Credits oder XP: Der Code folgt keiner Umbenennung.

Fehler: invalid_key (401, Session), validation_error (422, fehlerhafte E-Mail, kein Feld mitgeschickt, oder eine Wegwerf-/gesperrte E-Mail-Adresse), rate_limited (429).

POST /v1/ambassador-claim

Das angemeldete Mitglied gibt Credits für eine Prämie aus. Der Aufrufer nennt nie einen Preis: Kosten, Guthaben, Bestand, das Limit pro Mitglied und die Stufensperre werden alle aus dem Ledger unter einer Zeilensperre gelesen und durchgesetzt.

Scope write:ambassadors, plus Session. Nie zwischengespeichert. Nimmt Idempotency-Key an. Begrenzt auf 20 pro Minute und Besucher (client_ip).

FeldTypPflichtHinweise
reward_idstringjaEine UUID
answerstringneinHöchstens 500 Zeichen, wenn die Prämie eine Frage stellt

Antwort 201 bei Erfolg, trägt claim_id und status.

Fehler: invalid_key (401, keine Session), not_found (404, unbekannte Prämie), conflict (409, inactive, not_started, ended, out_of_stock, max_per_member, insufficient_credits), validation_error (403, member_not_active oder tier_locked; 422, answer_required oder ein fehlerhaft geformter Body).

Zwei Ablehnungen, member_not_active und tier_locked, antworten mit 403 und dem Code validation_error: Lies das Feld reason, um sie zu unterscheiden.

POST /v1/ambassador-email-preference

Das angemeldete Mitglied schaltet Programm-E-Mails aus, oder wieder ein.

Scope write:ambassadors, plus Session. Nie zwischengespeichert. Nimmt Idempotency-Key an. Begrenzt auf 20 pro Minute und Besucher (client_ip).

FeldTypPflichtHinweise
opt_outbooleanja

Ein Schalter, nicht die einseitige Abmeldung, die eine E-Mail-Fußzeile ausführt: Das Portal ist der eine Ort, an dem ein Mitglied beweisen kann, wer es ist, und seine Meinung wieder ändern kann.

Fehler: invalid_key (401, Session), validation_error (422, opt_out fehlt oder ist kein Boolean).

POST /v1/ambassador-logout

Widerruft die in X-Ambassador-Session genannte Session.

Scope write:ambassadors. Nie zwischengespeichert. Nimmt Idempotency-Key an.

Antwort: immer 200 { "ok": true }, ob die Session nun existierte oder nicht, und ob der Header überhaupt mitgeschickt wurde oder nicht: Abmelden ist kein Ort, um irgendetwas zu erfahren.

Fehler: keine über die Authentifizierung hinaus.

GET /v1/ambassador-code

Löst ein ?amb=CODE auf einer Ticketseite auf: Ist dieser Code auf dieser Veranstaltung aktiv, was ist er wert, und wem gehört er. Eine Vorschau, nie ein Preis: Die Konditionen werden im Checkout aus derselben Quelle erneut gelesen.

Scope read:ambassadors. 30 s zwischengespeichert. Begrenzt auf 60 Abfragen pro Minute und Schlüssel.

ParameterWerteStandardBedeutung
event_idpositive GanzzahlPflichtDie Veranstaltung, gegen die der Code geprüft wird
codeText, höchstens 64 ZeichenPflichtDer Code, wie eingetippt
{ "code": "MELANPAU", "discount_id": "f4c8b1a2-...", "ambassador_first_name": "Melanie", "percentage": 15, "amount": null, "min_order_amount": 0 }

discount_id ist das, was ein Shop an POST /v1/checkout-sessions als discount_id übergibt: Diese Route lehnt einen rohen Gutscheincode rundweg ab. Ein Code, der nicht existiert, nicht zu dieser Veranstaltung gehört, einem gesperrten Mitglied gehört, oder einem anderen Mandanten gehört, antworten alle mit demselben 404: Alles Genauere würde daraus ein Aufzählungs-Orakel über jeden Veranstalter der Plattform machen.

Fehler: validation_error (422, event_id / code fehlt oder ist fehlerhaft geformt), not_found (404), rate_limited (429).


MCP und Webhooks

Zwei weitere Wege zu denselben Daten, jeder auf seiner eigenen Seite dokumentiert:

  • POST /v1/mcp ist der Sway-MCP-Server: die Lese-Routen oben, bereitgestellt als schreibgeschützte Tools für einen KI-Assistenten über JSON-RPC (Streamable HTTP, zustandslos), hinter einem Schlüssel mit dem Scope mcp:access. Siehe Der Sway-MCP-Server.
  • Webhooks, eingerichtet in Crew-Admin → API → Webhooks, senden Ereignisse (bezahlte und erstattete Bestellungen, veröffentlichte und aktualisierte Veranstaltungen, Booking-Anfragen, Ambassador-Verkäufe, Formularantworten, Auszahlungen) an den eigenen Server eines Partners, sobald sie passieren, signiert nach der Standard-Webhooks-Art. Siehe Webhooks.