← Back to skills

Domain skill

eventbrite

Markdown synced from browser-harness domain skills.

Host
eventbrite
Files
1

Agent prompt

Use this skill

Copy this prompt into your coding agent to make it enable browser-harness domain skills and read this exact domain folder before automating.

Set up https://github.com/browser-use/browser-harness for me if it is not already installed. If setup is needed, read `install.md` first to install and connect it to my real browser. Then read `SKILL.md` for normal usage and always read `helpers.py` because that is where the browser-harness functions are.

Enable domain skills if they are not already enabled by setting `BH_DOMAIN_SKILLS=1` for browser-harness. Use the `eventbrite` domain skill from `agent-workspace/domain-skills/eventbrite/`. Read every markdown file for this domain before inventing an approach:
- agent-workspace/domain-skills/eventbrite/scraping.md

Use those domain-skill notes to complete my task for `eventbrite` in my real browser. When you open a setup, verification, or task tab, activate it so I can see the active browser tab.

Skill contents

What the agent will read

Scraping & Data Extraction

scraping.md

Source
  • https://www.eventbrite.com — public event listings and detail pages, no auth required for HTML scraping. REST API requires an OAuth token.
  • Use the search listing URL to get event lists — parse the ItemList JSON-LD block, not the HTML.
  • For a single event, fetch the detail page and extract the Event JSON-LD block. It contains all fields including offers (pricing). There is also a richer NEXTDATA block if you need venue coordinates, refund policy, or...
  • Location format: {state-abbreviation}--{city} (lowercase, hyphens for spaces)
  • ca--san-francisco
Show full markdown

https://www.eventbrite.com — public event listings and detail pages, no auth required for HTML scraping. REST API requires an OAuth token.

Do this first

Use the search listing URL to get event lists — parse the ItemList JSON-LD block, not the HTML.

python
import re, json

headers = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"}
html = http_get("https://www.eventbrite.com/d/ca--san-francisco/tech/", headers=headers)

ld_blocks = re.findall(r'<script type="application/ld\+json">(.*?)</script>', html, re.DOTALL)
for block in ld_blocks:
    parsed = json.loads(block)
    if isinstance(parsed, dict) and parsed.get('@type') == 'ItemList':
        for item in parsed['itemListElement']:
            ev = item['item']
            print(ev['name'], ev['startDate'], ev['url'])
        break
# Returns 18–40 events per page

For a single event, fetch the detail page and extract the Event JSON-LD block. It contains all fields including offers (pricing). There is also a richer __NEXT_DATA__ block if you need venue coordinates, refund policy, or sales status.

URL structure

Search / listing pages

code
https://www.eventbrite.com/d/{location}/{category}/
https://www.eventbrite.com/d/{location}/{category}/?page=2
https://www.eventbrite.com/d/{location}/{category}/?start_date=2026-05-01&end_date=2026-05-31

Location format: {state-abbreviation}--{city} (lowercase, hyphens for spaces)

  • ca--san-francisco
  • ny--new-york
  • ca--los-angeles
  • Use online for virtual events

Category slugs (confirmed working):

  • tech — Technology events
  • music — Music
  • food--drink — Food & Drink
  • health — Health & Wellness
  • sports--fitness — Sports & Fitness
  • arts--entertainment — Arts & Entertainment
  • family--education — Family & Education
  • business--professional — Business & Networking
  • science--tech — Science & Technology
  • community--culture — Community & Culture
  • networking — Networking
  • events — All events (broadest, returns ~40/page)

Filter slugs (replace category):

  • free--events — Free events only
  • events--today — Today
  • events--tomorrow — Tomorrow
  • events--this-weekend — This weekend

Query params:

  • ?page=N — Pagination (page 2+ confirmed working, each returns 18–20 events)
  • ?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD — Date range filter (confirmed, narrows results)

Event detail pages

code
https://www.eventbrite.com/e/{slug}-tickets-{event_id}

Example: https://www.eventbrite.com/e/icontact-the-tactile-tech-opera-tickets-1982861003639

  • event_id is a numeric string (10–13 digits)
  • Extract with: re.search(r'-tickets-(\d+)$', url).group(1)
  • Extract slug with: re.search(r'/e/(.+)-tickets-\d+$', url).group(1)

Other TLDs (.ca, .co.uk, etc.) use the same structure — event IDs are globally unique across TLDs.

Listing page: JSON-LD ItemList schema

The first <script type="application/ld+json"> block on any /d/ page is an ItemList. Each itemListElement contains:

json
{
  "position": 1,
  "@type": "ListItem",
  "item": {
    "@type": "Event",
    "name": "iContact the tactile tech opera",
    "description": "An immersive performance...",
    "url": "https://www.eventbrite.com/e/icontact-the-tactile-tech-opera-tickets-1982861003639",
    "image": "https://img.evbuc.com/...",
    "startDate": "2026-06-21",
    "endDate": "2026-06-21",
    "eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
    "location": {
      "@type": "Place",
      "name": "Little Boxes Theater",
      "address": {
        "@type": "PostalAddress",
        "addressLocality": "San Francisco",
        "addressRegion": "CA",
        "addressCountry": "US",
        "streetAddress": "94107 1661 Tennessee Street",
        "postalCode": "94107"
      },
      "geo": {
        "@type": "GeoCoordinates",
        "latitude": "37.7508806",
        "longitude": "-122.3881427"
      }
    }
  }
}

Note: listing-page items do NOT include offers (pricing) or organizer. Fetch the detail page for those.

The second JSON-LD block on listing pages is a BreadcrumbList (skip it).

Detail page: JSON-LD Event schema

The detail page has 4 JSON-LD blocks. The Event (or BusinessEvent) block is the second one and contains the full schema:

python
import re, json

headers = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"}
html = http_get("https://www.eventbrite.com/e/icontact-the-tactile-tech-opera-tickets-1982861003639", headers=headers)

ld_blocks = re.findall(r'<script type="application/ld\+json">(.*?)</script>', html, re.DOTALL)
event_data = None
for block in ld_blocks:
    parsed = json.loads(block)
    if isinstance(parsed, dict) and parsed.get('@type') in ('Event', 'BusinessEvent', 'MusicEvent', 'EducationEvent'):
        event_data = parsed
        break

print(event_data['name'])              # "iContact the tactile tech opera"
print(event_data['startDate'])         # "2026-06-21T17:05:00-07:00"  (ISO 8601 with TZ)
print(event_data['endDate'])           # "2026-06-21T20:08:00-07:00"
print(event_data['eventStatus'])       # "https://schema.org/EventScheduled"
print(event_data['eventAttendanceMode'])  # "https://schema.org/OfflineEventAttendanceMode"
print(event_data['location']['name'])  # "Little Boxes Theater"
print(event_data['location']['address']['streetAddress'])   # "94107 1661 Tennessee Street, San Francisco, CA 94107"
print(event_data['organizer']['name'])  # "Beth McNamara"
print(event_data['organizer']['url'])   # "https://www.eventbrite.com/o/beth-mcnamara-120755148166"

Full confirmed schema on detail page:

code
name               str     Event title
description        str     Short summary
url                str     Canonical event URL
image              str     Event banner image URL
startDate          str     ISO 8601 with timezone offset
endDate            str     ISO 8601 with timezone offset
eventStatus        str     URI: EventScheduled / EventCancelled / EventPostponed
eventAttendanceMode str    URI: OfflineEventAttendanceMode / OnlineEventAttendanceMode / MixedEventAttendanceMode
location.@type     str     "Place" (in-person) or "VirtualLocation" (online)
location.name      str     Venue name
location.address.streetAddress   str
location.address.addressLocality str    City
location.address.addressRegion   str    State abbreviation
location.address.addressCountry  str    Country code
organizer.name     str     Organizer display name
organizer.url      str     Organizer profile URL
offers             list    AggregateOffer object(s)

Offers / pricing

python
offers = event_data.get('offers', [])
if offers:
    offer = offers[0]   # always a list; typically one AggregateOffer
    print(offer['@type'])           # "AggregateOffer"
    print(offer['lowPrice'])        # "50.0"  (string, not float)
    print(offer['highPrice'])       # "50.0"
    print(offer['priceCurrency'])   # "USD"
    print(offer['availability'])    # "InStock" / "SoldOut"
    print(offer['availabilityStarts'])   # ISO 8601 UTC
    print(offer['availabilityEnds'])     # ISO 8601 UTC

# Free events: lowPrice="0.0", highPrice="0.0"
# Free check: float(offer['lowPrice']) == 0.0

@type on the event itself varies by format (all scrape identically):

  • Event — general
  • BusinessEvent — networking, professional
  • MusicEvent — concerts
  • EducationEvent — classes, workshops

Detail page: __NEXT_DATA__ (richer structured data)

Every event detail page embeds a <script id="__NEXT_DATA__"> block with additional fields not in JSON-LD:

python
import re, json

nextjs = re.search(r'<script id="__NEXT_DATA__"[^>]*>(.*?)</script>', html, re.DOTALL)
nd = json.loads(nextjs.group(1))
context = nd['props']['pageProps']['context']

bi = context['basicInfo']
print(bi['id'])               # "1982861003639"  (event ID string)
print(bi['name'])             # event title
print(bi['isFree'])           # bool
print(bi['isOnline'])         # bool
print(bi['currency'])         # "USD"
print(bi['status'])           # "live" / "completed" / "canceled"
print(bi['organizationId'])   # numeric string
print(bi['formatId'])         # numeric string (event format category)
print(bi['isProtected'])      # bool — password-protected events
print(bi['isSeries'])         # bool — recurring series
print(bi['created'])          # ISO 8601 UTC creation timestamp

# Venue with coordinates
venue = bi['venue']
print(venue['name'])                              # "Little Boxes Theater"
print(venue['address']['city'])                   # "San Francisco"
print(venue['address']['region'])                 # "CA"
print(venue['address']['latitude'])               # "37.7508806"
print(venue['address']['longitude'])              # "-122.3881427"
print(venue['address']['localizedMultiLineAddressDisplay'])  # list of strings

# Organizer details
org = bi['organizer']
print(org['name'])            # "Beth McNamara"
print(org['url'])             # organizer profile URL
print(org['numEvents'])       # int
print(org['verified'])        # bool

# Sales status
ss = context['salesStatus']
print(ss['salesStatus'])      # "on_sale" / "sold_out" / "sales_ended"
print(ss['startSalesDate']['local'])   # local datetime string

# Good to know
gtk = context['goodToKnow']['highlights']
print(gtk['ageRestriction'])          # "18+" or null
print(gtk['durationInMinutes'])       # int (e.g. 183)
print(gtk['doorTime'])                # local datetime string or null
print(gtk['locationType'])            # "in_person" or "online"

# Refund policy
refund = context['goodToKnow']['refundPolicy']
print(refund['policyType'])           # "custom" / "no_refunds" / "standard"
print(refund['isRefundAllowed'])      # bool
print(refund['validDays'])            # int or null

# Full event description (HTML)
for module in context['structuredContent']['modules']:
    if module['type'] == 'text':
        print(module['text'])         # raw HTML, may need BeautifulSoup to strip tags

Complete workflow: scrape events from a category

python
import re, json

def get_events_from_listing(location, category, page=1):
    """Returns list of event dicts with name, url, startDate, endDate, location."""
    headers = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"}
    url = f"https://www.eventbrite.com/d/{location}/{category}/?page={page}"
    html = http_get(url, headers=headers)
    ld_blocks = re.findall(r'<script type="application/ld\+json">(.*?)</script>', html, re.DOTALL)
    for block in ld_blocks:
        parsed = json.loads(block)
        if isinstance(parsed, dict) and parsed.get('@type') == 'ItemList':
            return [item['item'] for item in parsed.get('itemListElement', [])]
    return []

def get_event_detail(event_url):
    """Returns full Event JSON-LD + NEXT_DATA context for a single event."""
    headers = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"}
    html = http_get(event_url, headers=headers)

    # JSON-LD Event block
    ld_blocks = re.findall(r'<script type="application/ld\+json">(.*?)</script>', html, re.DOTALL)
    event_ld = None
    for block in ld_blocks:
        parsed = json.loads(block)
        if isinstance(parsed, dict) and parsed.get('@type') in ('Event', 'BusinessEvent', 'MusicEvent', 'EducationEvent'):
            event_ld = parsed
            break

    # NEXT_DATA context
    nextjs = re.search(r'<script id="__NEXT_DATA__"[^>]*>(.*?)</script>', html, re.DOTALL)
    context = None
    if nextjs:
        nd = json.loads(nextjs.group(1))
        context = nd['props']['pageProps']['context']

    return event_ld, context

# Usage
events = get_events_from_listing("ca--san-francisco", "tech", page=1)
print(f"Found {len(events)} events")  # 18–20 typical

for ev in events[:3]:
    print(ev['name'], ev['startDate'], ev['url'])

# Deep-fetch one event
ld, ctx = get_event_detail(events[0]['url'])
if ld and ld.get('offers'):
    price = float(ld['offers'][0]['lowPrice'])
    currency = ld['offers'][0]['priceCurrency']
    print(f"Price: {price} {currency}")   # 0.0 USD (free) or e.g. 50.0 USD

Public API: requires auth

The Eventbrite REST API (https://www.eventbriteapi.com/v3/) requires an OAuth token for all endpoints:

  • GET /v3/events/{id}/ — HTTP 401 without auth
  • GET /v3/events/search/ — HTTP 404 (endpoint changed; auth also required)

Use HTML scraping instead — the JSON-LD and __NEXT_DATA__ data is equivalent to the API response and requires no credentials.

If you have a token (EVENTBRITE_TOKEN):

python
import os
token = os.environ.get('EVENTBRITE_TOKEN')
headers = {
    "User-Agent": "Mozilla/5.0",
    "Authorization": f"Bearer {token}"
}
data = json.loads(http_get(f"https://www.eventbriteapi.com/v3/events/{event_id}/", headers=headers))

Gotchas

  • Event URLs in the HTML use relative /e/ paths, not absolute URLs — Search listing HTML contains /e/slug-tickets-id?aff=... relative paths (with tracking params). Extract event URLs from the JSON-LD ItemList instead — they are absolute, clean URLs without tracking params.

  • re.findall(r'href="https://www.eventbrite.com/e/...') returns 0 results — Confirmed: event cards in the HTML do not have https://www.eventbrite.com/e/ in href attributes. Use JSON-LD extraction only.

  • __SERVER_DATA__ does not exist — Both search and detail pages were checked. There is no window.__SERVER_DATA__ or window.__redux_state__. The embedded data is in <script id="__NEXT_DATA__"> (detail pages only) and JSON-LD (both).

  • Search listing pages have no __NEXT_DATA__ — Only event detail pages (/e/ URLs) have the __NEXT_DATA__ block. Listing pages (/d/ URLs) have JSON-LD only.

  • @type varies by event format — Don't filter JSON-LD blocks with parsed['@type'] == 'Event' alone. Check for any of: Event, BusinessEvent, MusicEvent, EducationEvent. They have identical field structure.

  • startDate on listing vs. detail pages differs in precision — Listing page items show date-only ("2026-06-21"). Detail page Event block shows full ISO 8601 with timezone offset ("2026-06-21T17:05:00-07:00"). Use detail page for scheduling tasks.

  • offers is absent on listing page items — The ItemList does not include pricing. Fetch the detail page for offers.lowPrice / offers.highPrice.

  • Free events have lowPrice: "0.0" and highPrice: "0.0" — Not null or missing. Check float(offers[0]['lowPrice']) == 0.0 or use basicInfo.isFree from __NEXT_DATA__.

  • offers prices are strings, not floats"50.0" not 50.0. Cast with float(offer['lowPrice']) before arithmetic.

  • Page size is ~18–20 events per page — Not a fixed 20. Some pages return fewer. Don't assume page N is empty because it returned < 20.

  • Date filter works but can still return events outside range — The ?start_date= / ?end_date= params narrow results but are not strict; always validate startDate from the returned data.

  • Eventbrite CA / UK / AU use different TLDs — Online event listings may surface eventbrite.ca, eventbrite.co.uk URLs. The /e/ structure and JSON-LD schema are identical. Fetch them with the same code.

  • No rate limiting observed — 8 sequential HTTP requests across 4 pages completed without errors or blocks (avg ~1.5s each). No delay needed for light workloads, but be reasonable for bulk scraping.