Discover Events and Parties on Luma
Purpose
Find public events, meetups, and parties on Luma (luma.com) by city or by interest category, returning a structured list of each event's name, canonical URL, start time, timezone, location/venue, hosts, hosting calendar, and ticket/RSVP info. Read-only — this skill only reads public discovery data; it never RSVPs, registers, buys tickets, or signs in.
Luma's web UI is a thin Next.js client over a public, unauthenticated JSON API at api.luma.com. The recommended path fetches that API directly — no browser, no login, no anti-bot stealth, no residential proxy. The browser flow is documented as a fallback only.
When to Use
- Find upcoming events/parties in a specific city ("what's happening in San Francisco / NYC / London this week").
- Browse events by interest category (Tech, AI, Crypto, Food & Drink, Arts & Culture, Climate, Fitness, Wellness).
- Monitor a city or category for new events on a schedule.
- Bulk-collect event listings (name, time, venue, hosts, RSVP status) across multiple cities or categories.
- Anywhere you'd otherwise scrape Luma's rendered HTML — the JSON API is faster, cheaper, and structurally stable.
Workflow
The optimal method is the public JSON API at api.luma.com/discover/get-paginated-events. It needs no API key, no cookies, no auth header, and no special request headers (the x-luma-* headers the web app sends are optional — server-to-server GETs return 200 without them). It also works without a residential proxy because results are geo-scoped by the latitude/longitude query params, not by your source IP. Lead with the API; only fall back to the browser if the API is ever blocked.
Step 1 — (Optional) Discover available cities and categories
To enumerate the cities and categories Luma surfaces (with their slugs, coordinates, and event counts), fetch the discover landing page and parse the embedded Next.js data — this is server-rendered, so a plain HTTP GET works:
GET https://luma.com/discover
Extract the <script id="__NEXT_DATA__"> JSON, then read props.pageProps.initialData:
categories[]→ each hascategory.slug(tech,ai,crypto,food,arts,climate,fitness,wellness),api_id, andevent_count.places[]→ each hasplace.slug(sf,nyc,london,singapore,la,berlin,tokyo, …),place.coordinate.{latitude,longitude}, andevent_count.featured_place.events[]→ ~12 fully-populated events for the viewer's nearest city (a quick zero-extra-call sample).
You can also skip this step entirely if you already know the slug you want (city slugs are the obvious abbreviations; the eight category slugs are listed above).
Step 2 — Fetch the event list
GET https://api.luma.com/discover/get-paginated-events
?slug={slug}
&pagination_limit={N} # e.g. 20
[&latitude={lat}&longitude={lon}]
[&pagination_cursor={cursor}] # for page 2+
Two scoping modes:
- By city (place slug) — e.g.
slug=sf. A place slug self-geo-scopes;latitude/longitudeare not required and are ignored if passed. This is the path for "events/parties in {city}". - By interest (category slug) — e.g.
slug=tech. A category slug requireslatitude&longitude— without coordinates the API returns{"entries": [], "has_more": false}(empty, not an error). Use the target city's coordinates from Step 1'splaces[], or any lat/lon you want to search around.
Response shape: { "entries": [...], "has_more": bool, "next_cursor": "<opaque-base64>" }.
Step 3 — Map each entry
Each element of entries[] wraps an event object plus enrichment fields. Pull:
event.name,event.start_at(UTC ISO),event.timezone(display tz),event.api_id- Canonical event URL:
https://luma.com/{event.url}—event.urlis the short slug (e.g.weavehacks→https://luma.com/weavehacks). event.location_type(offline|online),event.geo_address_info.city_stateand.full_address(null/omitted for online events or when the host hides the address).hosts[].name(array),calendar.name(the hosting calendar/community).ticket_info:{ is_free, price, is_sold_out, spots_remaining, is_near_capacity, require_approval }.guest_count(registered attendees; often0for newly listed events).
Step 4 — Paginate
If has_more is true, re-request the same URL adding pagination_cursor={next_cursor} from the previous response. Repeat until has_more is false. Keep the other params identical between pages.
Step 5 — "Parties" / filtering
Luma has no dedicated "parties" category — its taxonomy is the eight categories above. Treat "find parties" as: pull a city's event list (Step 2, place slug) and filter event.name / calendar.name / hosts client-side for party/nightlife/social keywords, and/or pull the food category for a target city. There is no working anonymous keyword-search endpoint (see Gotchas). Document this assumption in your output.
Browser fallback
Only if the API is unreachable. Anti-bot is heavier here (Cloudflare + Shape on the page shell — use --remote with --verified --proxies):
browse open https://luma.com/{slug} --remote(place slug → city event list; category slug → category page).browse wait timeout 3000thenbrowse get html body— the same event data is server-rendered into<script id="__NEXT_DATA__">. For a place page, events are embedded atprops.pageProps.initialData.data.events[](≈20 events). For a category page, events are not embedded (onlycategory+timeline_calendars); the page lazy-loads them from the sameget-paginated-eventsAPI, so prefer the API directly.- To see rendered cards,
browse mouse scroll 640 400 0 900thenbrowse screenshot. Do not click into events, RSVP, or subscribe — read-only.
Site-Specific Gotchas
- API host is
api.luma.com, notapi.lu.ma. The legacyapi.lu.mahost is live but returns{"message":"Not found."}for these discover paths — don't waste time on it. - Category slugs require coordinates; place slugs don't.
slug=techwith nolatitude/longitudereturns an emptyentriesarray (HTTP 200, not an error) — easy to misread as "no events." Always passlatitude+longitude(from a city'splaces[]entry) for category slugs. Place slugs likesfself-scope. - Geo-scoping is by query param, not source IP. A residential proxy is not required for the API. Confirmed: the same
slug=sfquery returns identical results with and without--proxies. The pre-run anti-bot probe (likelyNeedsVerified/Proxies: true) applies to the rendered page shell (Cloudflare + Shape), not to the JSON API. - No auth, no special headers needed. The web app sends
x-luma-client-type,x-luma-timezone,x-luma-web-url,x-luma-client-version, but the API returns200for plain server-to-server GETs without any of them. No cookie or bearer token required. - No anonymous keyword search.
api.luma.com/search/get-results?query=...returns HTTP 401 (auth required) andapi.luma.com/discover/searchreturns 404. There is no public free-text search — discovery is slug-based (city or category) only. "Parties" must be filtered client-side from a city's list. event.urlis a bare slug, not a full URL. Build the canonical link ashttps://luma.com/{event.url}. It is NOT the same asevent.api_id(evt-…).next_cursoris opaque (base64-ish blob encoding the last item's sort value + id). Pass it back verbatim aspagination_cursor; don't try to decode or construct it.pagination_limitcaps page size. Observed working values up to ~20–25. Usehas_more+next_cursorto walk the full list rather than requesting a huge limit.start_atis UTC ISO; render withevent.timezone. The card UI shows local time using the event'stimezonefield — convert accordingly.- Address can be null/hidden.
geo_address_infois absent forlocation_type: "online"events and when a host gates the exact address behind RSVP approval (require_approval: true).city_stateis usually still present even whenfull_addressis hidden. hosts[]can contain duplicates (the same person listed twice, once as organizer and once as co-host) — dedupe by name if it matters.- Discover landing data is SSR'd at
props.pageProps.initialData(cities + categories + a featured city's events), but city/category sub-pages push the event list to a client-sideget-paginated-eventscall — so for anything beyond the discover home, hit the API, not the page HTML.
Expected Output
A list of events for the requested city or category, with pagination state. Example (place slug sf, pagination_limit=3):
{
"slug": "sf",
"scope": "place",
"total_returned": 3,
"has_more": true,
"next_cursor": "eyJzdiI6IjIwMjYtMDYtMDYgMjA6MDA6MDArMDAiLCJmYiI6ImV2dC1zRFM5TUNKRkRFR1dsdWEifQ",
"events": [
{
"api_id": "evt-QuGEMJl1hsIvImo",
"name": "WeaveHacks 4: Multi-Agent Orchestration Hackathon with Weights & Biases",
"url": "https://luma.com/weavehacks",
"start_at": "2026-06-06T16:00:00.000Z",
"timezone": "America/Los_Angeles",
"location_type": "offline",
"city": "San Francisco, CA",
"venue": "400 Alabama St ste 202, San Francisco, CA 94110, USA",
"hosts": ["Weights & Biases", "Alex Volkov", "Anna Shive"],
"calendar": "Weights & Biases",
"is_free": false,
"price": null,
"is_sold_out": false,
"spots_remaining": 5,
"guest_count": 0
},
{
"api_id": "evt-0mdEDP4pw0YTS2M",
"name": "Design Meetup x Reve Makeathon",
"url": "https://luma.com/ldhaw009",
"start_at": "2026-06-06T20:00:00.000Z",
"timezone": "America/Los_Angeles",
"location_type": "offline",
"city": "Palo Alto, CA",
"venue": null,
"hosts": ["Ilyssa Yan", "Kyra Mo", "Chi Quach", "Reve"],
"calendar": "Design Meetup",
"is_free": false,
"price": null,
"is_sold_out": false,
"spots_remaining": null,
"guest_count": 75
}
]
}
Category-scoped request (note required coordinates) and the empty-result shape to guard against:
// GET .../get-paginated-events?slug=tech&latitude=37.7749&longitude=-122.4194&pagination_limit=20
// -> { "entries": [ ... ], "has_more": true, "next_cursor": "..." }
// GET .../get-paginated-events?slug=tech (category slug, NO coords)
// -> { "entries": [], "has_more": false } // empty, NOT an error — coords were missing