Amsterdam · read-only · JSON

Every event in Amsterdam, as one JSON feed.

Thirteen ingest sources — cinema programmes, ticket sites, venue calendars, city permits — read on a schedule, deduplicated against each other, categorised, and served as a single paginated endpoint. You write one integration instead of thirteen scrapers.

10 requests, 24 hours, no card. Mint it in the playground or with one POST.

one request
# every music event in the next week, three at a time
curl -s https://events.jetskibay.com/v1/events \
  -H "x-api-key: $EVENTS_KEY" \
  --get \
  -d "category=music" \
  -d "from=2026-09-12" \
  -d "to=2026-09-19" \
  -d "limit=3"

# {
#   "events": [
#     { "title": "Nubya Garcia",
#       "category": "music",
#       "starts_at": "2026-09-12T20:30:00+02:00",
#       "venue_name": "Paradiso",
#       "price_min": 24.5, "is_free": false }, …
#   ],
#   "cursor": "b2Zmc2V0OjMy…"
# }
13ingest sources behind one catalogue
12categories, a closed set you can switch on
10requests on a trial key
24hbefore that key expires

What is in the data

One row per thing that is actually happening.

Not one row per listing. A film with twenty screenings, a gig that three ticket sites all announced, a market that also appears as a city permit — those collapse into the records a person would call one thing, with a real start time, a real venue, and a price when a price exists.

deduplicated

Thirteen feeds, one truth

The same concert arriving from a venue calendar, an aggregator and a ticket page becomes one record. You do not write the matching logic.

categorised

A closed set, not free tags

Every record carries exactly one of twelve categories. It is a fixed vocabulary, so ?category= is a filter you can build a UI on rather than a guess.

resolved

Real times, real places

starts_at is ISO 8601 with an Amsterdam offset — not "Friday evening". venue_name and address are filled where the source gave them.

honest

Nulls where we do not know

price_min, image_url, ends_at are nullable and often null. Nothing is invented to fill a column.

one event
{
  "id": "7b3f1a2c-9e04-4d61-8a77-2c1b5f0e9d33",
  "title": "Nubya Garcia",
  "description": "The London saxophonist brings her quartet…",
  "category": "music",
  "type": "event",
  "starts_at": "2026-09-12T20:30:00+02:00",
  "ends_at": "2026-09-12T23:00:00+02:00",
  "is_all_day": false,
  "venue_name": "Paradiso",
  "address": "Weteringschans 6-8",
  "city": "Amsterdam",
  "url": "https://www.paradiso.nl/en/program/nubya-garcia/…",
  "image_url": "https://…/nubya.jpg",
  "price_min": 24.5,
  "price_max": 24.5,
  "is_free": false
}

Every field, with its type and when it is null: the event record.

Narrowing it

Nine query parameters, and none of them are page numbers.

Date range, category, source, city, free text, price-free, page size, cursor. Ask for an unknown category and you get a 400 with a named error — not an empty page that looks like a quiet Tuesday.

The twelve categories

music film theatre festival market sport expo nightlife family food street_event other
# free, family-friendly, this weekend
curl -s https://events.jetskibay.com/v1/events \
  -H "x-api-key: $EVENTS_KEY" \
  --get \
  -d "category=family" \
  -d "is_free=true" \
  -d "from=2026-09-12" \
  -d "to=2026-09-14"

Types and accepted values for all nine: the filter reference.

Where it comes from

Thirteen adapters, named and health-checked.

Every record came in through exactly one adapter, and ?source= filters on which. GET /v1/sources returns this catalogue live, each entry carrying the health of its last ingest run — so a source that has gone quiet is visible to you, not just to us. No key needed for that route.

cinevilleCineville Movies — films & screenings for the coming week
uitkijkDe Uitkijk Movies — films not already covered by Cineville
google-eventsGoogle Events — Amsterdam events via SerpAPI
gemeentebladGemeenteblad Permits — announced event permits
eventbriteEventbrite — Amsterdam events
activiteitenActiviteiten Amsterdam — city activities
modMusic of Our Desire — concerts
jazzinJazz in Amsterdam — live music & jazz gigs
jazzin-theaterJazzin Theatre — theatre performances
jazzin-clubJazzin Club — club nights
jazzin-filmJazzin Film — screenings not already covered by Cineville
jazzin-independentJazzin Independent — grassroots & independent venues
marriottMarriott Events — hotel events

Before you build

Two things that will not behave the way you expect.

Both are deliberate, both are cheap to handle, and both are much cheaper to handle now than after your integration is written.

The key is returned in the response, not emailed. This host has no mail binding, so POST /v1/keys hands you the plaintext key in its own reply. That is the only time it exists anywhere you can read it — we keep a hash, so it cannot be looked up again, only replaced.

Which means: capture it from that one response, and treat the response body the way you would treat a password. It is an acceptable trade for a read-only API over cached public data. It would not be for an API that spends money.

Pagination is cursor-based, not page or offset. The ingest cron rewrites the table underneath you, so a reader walking ?page=2, 3, 4 would silently skip rows that moved up and repeat rows that moved down.

GET /v1/events returns an opaque cursor; pass it back as ?cursor= to get the page after it. Do not parse it, build one, or store it as a bookmark — its contents are ours to change. When there is no next page, there is no cursor.

The playground walks two pages with the real cursor, if you would rather see it than read it.

Get started

A trial key takes one request and no conversation.

Give an email address, get a key good for 10 requests that expires in 24 hours. Asking again for the same address carries whatever allowance is left onto the new key and revokes the old one — so losing a key costs nothing, and asking twice buys nothing.

or from a terminal
curl -s -X POST https://events.jetskibay.com/v1/keys \
  -H "content-type: application/json" \
  -d '{"email":"you@company.com"}'

# {
#   "key": "evk_9f2c41a8b03d5e6712c4f890…",  ← the only copy
#   "request_limit": 10,
#   "requests_remaining": 10,
#   "expires_at": "2026-09-13T09:12:44.000Z"
# }