Authenticatie

API-sleutels, scopes, rate limits, caching, fouten en idempotentie in de publieke Sway-API.

Elk verzoek draagt een API-sleutel als Bearer-token:

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

De sleutel identificeert je crew. Nergens in de API bestaat een tenant-parameter: wat een sleutel kan zien, wordt bepaald door de crew waartoe hij hoort, nooit door het verzoek.


Twee soorten sleutel

Secret keyPublishable key
Voorvoegselsway_sk_sway_pk_
Draait inJe serverDe browser van een bezoeker
ScopesElke scopeAlleen de publieke leesscopes: read:profile, read:artists, read:promoters, read:events, read:venues, read:content, read:shop
Waar hij antwoordtOveralAlleen op de websites die op de sleutel staan (gecontroleerd tegen de Origin van de browser)
Verstuurd alsAuthorization: Bearer ...Authorization: Bearer ..., of ?key= wanneer een header niet praktisch is

Gebruik een secret key voor alles wat je server doet, en voor alles wat schrijft, tickets verkoopt of leden raakt. Stuur hem nooit mee naar een browser of een app: wie je pagina's kan lezen, kan de sleutel lezen.

Gebruik een publishable key wanneer een browser de API rechtstreeks moet aanroepen, bijvoorbeeld bij een statische site zonder server. Hij leest alleen wat Sway al publiek toont, antwoordt alleen op zijn eigen websites, en krijgt daarbovenop een kleiner budget per bezoekersadres.

Een secret key verstuurd als ?key= wordt geweigerd met 400 key_in_query, omdat URL's terechtkomen in logs en browsergeschiedenis. Gebeurt dat toch, behandel de sleutel dan als gelekt: roteer hem.

Levenscyclus van een sleutel

Sleutels beheer je in Crew admin → API (abonnementen Studio en Roster, recht API beheren).

ActieWat er gebeurt
AanmakenNaam, type, scopes, websites of storefront-adressen, en een vervaldatum (nooit, 30, 90 of 365 dagen). De volledige sleutel wordt één keer getoond; daarna zie je alleen het begin en de laatste vier tekens
BewerkenWijzigt de scopes en adreslijsten; geldt meteen
SchorsenDe noodstop. Elk verzoek antwoordt 403 key_suspended tot iemand op Hervatten klikt. Er wordt niets verwijderd
RoterenPOST /v1/keys/rotate met een sleutel die write:keys heeft, geeft zijn vervanger terug: dezelfde naam, type, scopes en limieten. De oude sleutel blijft werken voor grace_hours (1 tot 168, standaard 24), en verloopt daarna. Een sleutel roteert maar één keer, hoogstens één keer per uur, en de crew-eigenaar krijgt er telkens een e-mail over
IntrekkenDefinitief. Verzoeken antwoorden 401 invalid_key. Is de sleutel geroteerd, dan trekt Ook de sleutels intrekken die hem vervingen de hele keten in: gebruik het wanneer de sleutel gelekt is, want wie een gelekte sleutel met write:keys heeft, kan hem geroteerd hebben
VerlopenNa expires_at antwoorden verzoeken 401 invalid_key

Een ingetrokken, verlopen of onbekende sleutel antwoorden allemaal met dezelfde 401 invalid_key: de API vertelt een aanroeper nooit of een sleutel die hij niet kent, ooit bestaan heeft. Het paneel Activiteit op elke sleutel toont wat hij van zijn limieten gebruikt heeft en zijn laatste aanroepen.

Loopt het abonnement van de crew af, dan antwoordt elke sleutel met 403 subscription_required tot het weer actief is.


Scopes

Scopes kies je bij het aanmaken van de sleutel en kun je later bewerken. Een verzoek zonder de scope die zijn endpoint nodig heeft, antwoordt 403 missing_scope.

ScopeOpentStandaard
read:profileHet eigen profiel van de crew: /v1/crew, /v1/crew/pagesJa
read:artistsHet roster, elke artiest, hun evenementen, kalenders en perskitsJa
read:promotersDe promotors die je beheert, hun evenementen, locaties en artiestenJa
read:eventsEvenementen, line-ups, dagen, timetables, tickettypes, beschikbaarheid, kosten, galerij, voorverkopenJa
read:venuesLocaties, hun evenementen en capaciteitJa
read:contentNieuws, partners, formulieren en perskitsJa
read:ambassadorsDe publieke pagina van het ambassadeursprogramma, ranglijst, rewards en quests, en de eigen pagina van een ingelogd lidNee
read:ambassador-membersDe leden, hun puntengeschiedenis en de programmastatistieken. Persoonsgegevens: alleen de crew-eigenaarNee
read:exportDe hele catalogus als NDJSON-downloadsNee
read:shopDe merch van de pagina's die je beheert: catalogus, product, wat een event verkoopt; het berekenen en controleren van een winkelmandNee
write:bookingsBookingaanvragen versturen (POST /v1/booking-requests)Nee
write:newsletterNieuwsbriefinschrijvingen toevoegen aan de CRM van een promotor, en een adres eraf halenNee
write:formsAntwoorden op je publieke formulieren insturenNee
write:checkoutBetaalde ticketcheckouts openen, en coupons en toegangscodes controlerenNee
write:checkout:freeOok gratis en 100%-kortingsbestellingen aanmaken. Alleen de crew-eigenaarNee
write:shop:checkoutMerch-checkouts openen voor een pagina die je beheertNee
write:ambassadorsAanmelden, in- en uitloggen, een reward claimen, eigen gegevens bewerkenNee
write:keysDe sleutel zichzelf laten roteren (POST /v1/keys/rotate)Nee
mcp:accessDe sleutel gebruiken met de MCP-serverNee

GET /v1/me, /v1/usage, /v1/audit-log en /v1/genres hebben geen scope nodig: elke geldige sleutel mag ze aanroepen.

De scopes voor "alleen de crew-eigenaar" kan alleen de crew-eigenaar toekennen; een lid met het recht API beheren kan ze verwijderen maar niet toevoegen. GET /v1/scopes geeft deze lijst terug als JSON.

Websites en storefront-adressen

Twee adreslijsten leven op een sleutel, allebei ingesteld in Crew admin → API:

  • Websites (publishable keys): de enige browserorigins waarop de sleutel antwoordt. Een verzoek van een andere site antwoordt 403 origin_not_allowed.
  • Storefront-adressen (sleutels met een ambassadeursscope): waar Sway een lid naar mag terugsturen na een ingemailde inloglink. Je site geeft het terugkeeradres mee met het verzoek; Sway aanvaardt het alleen als de origin op de lijst staat, zodat de link nooit ergens anders naartoe kan wijzen. Een sleutel met een ambassadeursscope en een lege lijst bedient nog steeds pagina's, maar kan nooit een lid opnieuw laten inloggen.

Alleen https-origins worden aanvaard, plus http op localhost voor ontwikkeling. Paden vallen weg: alleen de origin wordt bewaard en vergeleken.


Rate limits

De API is zo gebouwd dat een drukke partnersite zijn gegevens blijft tonen. Twee ideeën dragen dat:

  • Een gecachet antwoord is gratis. Reads worden gecachet per crew. Een verzoek herhalen dat de cache al heeft, kost niets, en een 304 Not Modified kost niets.
  • Werk telt, geen verzoeken. Alleen een verzoek dat de database moet lezen, spendeert het werkbudget van de sleutel, en het spendeert in verhouding tot wat het leest.
PlanWerk per minuutWerk per dag
Studio12020.000
Roster300100.000

Wat één ongecachet verzoek kost:

VerzoekWerkeenheden
Een detail (/v1/events/{id})1
Een lijstpagina1 per begonnen blok van 25 items (limit=100 kost 4)
Elke expand-waarde+1
Een batchopvraging (ids=)1 per begonnen blok van 10 ids

Raakt het budget op, dan wordt een read die de cache heeft nog steeds beantwoord, vanuit de laatste kopie, met Cache-Status: sway; hit; ...; detail=rate-limited. Alleen een read zonder enige kopie antwoordt 429 rate_limited, met Retry-After.

Andere budgetten, per sleutel tenzij anders vermeld:

BudgetWaarde
Verzoeken, gecachet of niet2.400 per minuut op Studio, 6.000 op Roster (of 20 keer het werk per minuut van de sleutel, wat hoger is)
Antwoordgegevens5 GB per dag op Studio, 20 GB op Roster
Alle sleutels van de crew samen600 werk per minuut en 120.000 per dag op Studio; 1.500 en 500.000 op Roster. Evenveel writes daarbovenop
Writes (POST), checkout apartZoveel als het werk per minuut van de sleutel, nooit minder dan 60
Checkoutsessies600 per minuut, voor de sleutel en voor alle sleutels van de crew samen, los van de andere writes
Controles van coupons en toegangscodes300 per minuut, los van de writes
MCP-berichten120 per minuut
Publishable key, per bezoekersadres60 werk en 600 verzoeken per minuut

Elk geauthenticeerd antwoord meldt het werkbudget van de sleutel:

RateLimit-Policy: "burst";q=120;w=60, "daily";q=20000;w=86400
RateLimit: "burst";r=118;t=42, "daily";r=19873;t=51230
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 1759525489

RateLimit-Policy en RateLimit volgen de IETF-draft; de X-RateLimit-*-headers tonen het strengste van de twee vensters en blijven bestaan voor bestaande clients. Bij een 429 wacht je Retry-After seconden; een sleutel die de limiet blijft raken, krijgt langere wachttijden. Een sleutel die ids op een ongewoon patroon doorloopt, wordt een tijdje beperkt tot gecachete antwoorden.

Met If-None-Match en de cache blijft een partnersite die duizenden bezoekers per dag bedient ver onder deze cijfers. De integratiegids toont het patroon. Heb je meer nodig, neem dan contact op: limieten worden per sleutel ingesteld.

Caching

Elke cachebare read draagt zijn levensduur en een ETag:

Cache-Control: private, max-age=60, stale-while-revalidate=120, stale-if-error=86400
ETag: "4f9a1c7e2b8d5f3a"
Cache-Status: sway; hit; ttl=42

Stuur de ETag terug als If-None-Match: is er niets veranderd, dan is het antwoord 304 Not Modified, zonder body en zonder kosten. Cache-Status zegt waar een antwoord vandaan komt: hit (de cache), fwd=miss (vers gelezen), fwd=stale (ververst nadat de levensduur afliep). Levensduren lopen van 30 seconden (tickettypes, beschikbaarheid) tot een uur (genres); de endpoint-referentie geeft ze allemaal. Antwoorden met gegevens op sleutel- of ledenniveau worden nooit gecachet (Cache-Control: private, no-store).

Wil je de kopie van Sway overslaan, stuur dan Cache-Control: no-cache mee met een secret key: het verzoek wordt vers gelezen (Cache-Status: sway; fwd=request; stored) en kost werk zoals elk ongecachet verzoek. Dat is het verzoek om te doen wanneer een webhook zegt dat er iets veranderd is, want de kopie kan van voor de wijziging dateren. Is je werkbudget op, dan krijg je de kopie toch, met detail=rate-limited. De Cache-Control van een publishable key wordt genegeerd.


Fouten

Elke fout is een RFC 9457 application/problem+json-body met een stabiele code om op te vertakken, en het id van het verzoek:

{
  "type": "https://www.sway.events/en/docs/api/errors#missing_scope",
  "title": "Missing Scope",
  "status": 403,
  "detail": "This API key does not have the required scope.",
  "instance": "/api/v1/events/4521",
  "code": "missing_scope",
  "request_id": "0b7c2e4a-9f1d-4a3b-8c6e-5d2f1a9b7e3c"
}
codeStatusBetekenis
invalid_key401De sleutel is misvormd, onbekend, ingetrokken of verlopen
key_in_query400Een secret key is verstuurd als ?key=. Roteer hem
key_suspended403De sleutel is geschorst; een crewbeheerder kan hem hervatten
subscription_required403Het abonnement van de crew bevat de API niet meer
missing_scope403De sleutel mist de scope van het endpoint, of een publishable key riep een endpoint aan dat een secret key nodig heeft
origin_not_allowed403Een publishable key werd gebruikt vanaf een website die niet op zijn lijst staat, of een browser riep de MCP-server aan
not_found404De bron bestaat niet of ligt buiten de pagina's van je crew
conflict409Dezelfde Idempotency-Key wordt nog verwerkt, of de actie is al gebeurd (een sleutel roteert maar één keer)
validation_error400 / 422Een queryparameter (400) of de request body (422) is ongeldig
invalid_cursor400De pagineringscursor is misvormd of verouderd
idempotency_mismatch422Deze Idempotency-Key is gebruikt met een andere body
password_required401Een beveiligde perskit heeft Presskit-Password nodig
rate_limited429Een limiet is bereikt; zie Retry-After
internal_error500Er ging iets mis aan onze kant; veilig om opnieuw te proberen
upstream_error502Een dienst waar Sway van afhangt (betalingen, e-mail) is uitgevallen; veilig om opnieuw te proberen

validation_error draagt ook een array errors met { "param", "message" }. Endpoints voegen hun eigen codes toe waar nodig (een uitverkocht tickettype, een code die niet geldt); de endpoint-referentie somt ze op.

404 not_found staat voor zowel "ontbreekt" als "niet van jou": de API bevestigt nooit dat er gegevens bestaan buiten je crew.

Elk antwoord, geslaagd of fout, draagt X-Request-Id. Noem het als je contact opneemt: het vindt het verzoek terug in onze logs. Je mag je eigen X-Request-Id meesturen (8 tot 128 letters, cijfers, ., _, : of -) om onze logs aan de jouwe te koppelen; al de rest wordt vervangen door een vers id.


Idempotentie

Een netwerk-retry mag nooit twee keer boeken, aanmelden of terugbetalen. Stuur een header Idempotency-Key mee op een POST, een unieke waarde per operatie (een UUID is ideaal, 1 tot 255 zichtbare ASCII-tekens):

curl -X POST https://www.sway.events/api/v1/booking-requests \
  -H "Authorization: Bearer sway_sk_YOUR_KEY" \
  -H "Idempotency-Key: 5b8f3c1e-2a7d-4e9b-8c6f-1d3a5e7b9c2f" \
  -H "Content-Type: application/json" \
  -d '{ "artist_ids": [501], "event_name": "Warehouse Series #4", "contact_email": "[email protected]" }'
  • Dezelfde sleutel met dezelfde body binnen 24 uur krijgt het eerste antwoord terug, zonder het werk opnieuw te doen.
  • Dezelfde sleutel met een andere body antwoordt 422 idempotency_mismatch.
  • Een retry die binnenkomt terwijl het eerste verzoek nog loopt, antwoordt 409 conflict: wacht en probeer opnieuw.

Het werkt op elke POST behalve het openen van een checkout, dat zelf al tegen duplicaten waakt, en de controles van coupons en toegangscodes, die niets veranderen. Een antwoord dat een secret draagt (een geroteerde sleutel) wordt nooit bewaard voor een replay.