By the end of this tutorial, you will have a short Python script that turns a dice.fm city into structured rows: event name, start and end time with the venue's UTC offset, venue and address, ticket status, currency and price.
dice.fm lists gigs, club nights, comedy, theatre and talks city by city. Its city page shows 30 "popular" events; the full catalogue sits behind a "Load more" button.
In this guide, we use Python and the Apify Actor DICE Events Scraper (reventadata/dice-scraper). It is unofficial and not affiliated with DICE. You will configure a city search, run the Actor and filter rows by keyword, price and ticket status. The sample row shown below is a sample from a capture dated 2026-10-21, parsed with the Actor's own parser.
⏱️ Time estimate: one sitting: install one package, set one token, run three short Python blocks.
Prerequisites
To follow this tutorial, you will need:
-
Python 3.11+ (
apify-client3.3.0 requires it). - The Apify API client for Python.
- An Apify account and its API token, which sits on the API & Integrations page in Apify Console.
Install the client:
pip install "apify-client~=3.0"
The code reads the token from an environment variable:
export APIFY_API_TOKEN="your_apify_token_here"
What you'll achieve
You will turn one or more dice.fm cities into one row per event. Each row carries the event id and url, name, status (on-sale, sold-out, announced or off-sale), startDate and endDate, the ticket timeline (announcedAt, onSaleAt, onSaleEndsAt), the first venue's name, address and city, every venue in venues, and currency with price or priceFrom.
Note: The Actor reads public event listings only, nothing from behind a login. Event descriptions, line-ups and ticket tiers are not included; the price is the lowest one the listing card shows.
Example use case: Suppose you want London gigs with tickets at £10 or less that are still on sale. You would set cities: ["London, GB"], categories: ["music:gig"], maxPrice: 10 and onlyOnSale: true, and every saved row would be an on-sale gig whose cheapest ticket is at most 10 GBP.
Step 1: Access DICE Events Scraper
Go to the DICE Events Scraper page on Apify Store. The README lists every input and output field. In this tutorial we drive the Actor from Python, so everything after this step happens in code.
Step 2: Configure the search
The one required input is cities: dice.fm cities written as the site shows them, such as Paris, New York or Berlin. The city list captured on 2026-10-21 held 98 cities, 43 of them in the US and 17 in the UK. London and Southampton exist in two countries, so write them with the country: London, GB, London, CA, Southampton, GB or Southampton, US.
The optional categories input takes DICE tags, one search per city and tag. The 16 main ones are music:gig, culture:comedy, music:dj, music:party, culture:social, culture:art, culture:theatre, culture:talks, culture:workshop, culture:film, culture:sport, culture:foodanddrink, music:artistsigning, music:playback, culture:podcast and culture:wellbeing. Sub-genres such as gig:jazz work too. Leave it empty for every category.
maxItems (default 100) caps the events saved across all cities and categories.
Here is the setup. It searches London gigs and saves at most 20 events:
import os
from apify_client import ApifyClient
# 1. Initialize ApifyClient with the token from the environment
client = ApifyClient(os.environ["APIFY_API_TOKEN"])
# 2. Define the search: London gigs, at most 20 events
run_input = {
"cities": ["London, GB"],
"categories": ["music:gig"],
"maxItems": 20,
}
💡 Tip: DICE returns events within a radius of the city centre, so a London search also holds venues in nearby towns. On the captured page, 20 of 22 events were in London, 1 in Luton and 1 in Chelmsford. The city field says where each venue is.
No proxy setting is needed in run_input: the proxy is off by default, and dice.fm answered without one from the Apify platform in testing on 2026-10-21.
Step 3: Add optional filters
Two inputs narrow the search on the site itself, and three run on the rows after the fetch. Rows a filter drops never reach your dataset:
| Parameter | Type | What it does |
|---|---|---|
dateFrom / dateTo
|
string | Date range, YYYY-MM-DD, sent to the site. Empty means today (UTC) to two years later. |
keywords |
array | Keeps events whose name, one-liner or venue contains any of the words, ignoring case and accents. |
maxPrice |
number | Keeps events whose cheapest ticket is at or under the value, in the event's own currency. Events with no price are dropped. |
onlyOnSale |
boolean | Keeps only events with status on-sale. |
Three example configurations:
-
Comedy this weekend:
cities: ["London, GB"],categories: ["culture:comedy"],dateFromanddateToset to the Saturday and Sunday. -
Cheap jazz in Paris:
cities: ["Paris"],categories: ["gig:jazz"],maxPrice: 15. -
Two cities, one dataset:
cities: ["Berlin", "Barcelona"],onlyOnSale: true.
⚠️ A city name dice.fm does not know, or London without a country, stops the run with an error that lists the valid names. A date not written
YYYY-MM-DDis refused before the run starts, and adateTobeforedateFromstops the run at its start.
Step 4: Run the Actor
Now start the run with client.actor("reventadata/dice-scraper").call(run_input=run_input). The call starts the Actor on Apify and waits for it to finish before returning the run:
# 3. Start the Actor run and wait for it to finish
print("Starting DICE extraction...")
run = client.actor("reventadata/dice-scraper").call(run_input=run_input)
if run is None or run.status != "SUCCEEDED":
status = run.status if run else "unknown"
raise SystemExit(f"Run did not succeed (status: {status}).")
# 4. Fetch the saved events
items = client.dataset(run.default_dataset_id).list_items().items
print(f"Retrieved {len(items)} events.\n")
Behind the scenes:
- The Actor reads dice.fm's city list, then sends each city's coordinates, tag and date range to the same search the site's "Load more" button uses.
- It pages through the results in date order, skips any event it already saved, applies your filters, and saves the rows.
- It stops at
maxItemsor when the site has no next page.
Step 5: View and download results
Loop over the rows to print the parts you need:
for item in items:
print(item["name"])
print(f" Starts: {item.get('startDate')}")
print(f" Venue: {item.get('venue')} ({item.get('city')})")
print(f" Status: {item.get('status')}")
if item.get("price") is not None:
print(f" Price: {item['price']} {item['currency']}")
elif item.get("priceFrom") is not None:
print(f" From: {item['priceFrom']} {item['currency']}")
print("-" * 40)
Here is one row, a sample from a capture dated 2026-10-21 (page 1 of the London music:gig search) parsed with the Actor's own parser. A saved row also carries scrapedAt, the time of the scrape:
{
"id": "6a6b2130ea0b950001491555",
"url": "https://dice.fm/event/6a6b2130ea0b950001491555",
"name": "Curió Curió",
"status": "on-sale",
"startDate": "2026-10-21T19:00:00+01:00",
"endDate": "2026-10-21T22:30:00+01:00",
"timezone": "Europe/London",
"isMultiDay": false,
"announcedAt": "2026-07-30T00:00:00+01:00",
"onSaleAt": "2026-07-30T00:00:00+01:00",
"onSaleEndsAt": "2026-10-21T19:00:00+01:00",
"preSaleAt": null,
"venue": "The Jazz Cafe",
"venueAddress": "5 Parkway, London NW1 7PG",
"venueSpace": null,
"venueId": "415",
"city": "London",
"country": "United Kingdom",
"countryCode": "GB",
"venues": [
{
"id": "415",
"name": "The Jazz Cafe",
"address": "5 Parkway, London NW1 7PG",
"space": null,
"city": "London",
"country": "United Kingdom",
"countryCode": "GB"
}
],
"currency": "GBP",
"price": 20.75,
"priceFrom": null,
"oneLiner": "Psychedelic disco and analogue soul",
"imageUrl": "https://dice-media.imgix.net/attachments/2026-07-30/b516d037-c5a1-4392-8799-9d80f1e3b2eb.jpg?rect=269%2C0%2C1295%2C1295",
"acquisitionType": "purchase",
"searchCity": "London, GB",
"searchCategory": "music:gig"
}
Apify datasets export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL.
Understanding your results
The fields that matter most:
-
priceandpriceFrom: never both; on every captured row, one was set.priceis a single ticket price;priceFromis the lowest of several tiers. The site stores prices in hundredths and the Actor divides by 100. In the 22-row capture, 16 rows hadprice, 6 hadpriceFrom, and 7 events were free (a price of 0). -
currency: the code as the site gives it (GBPon all 22 captured rows). The README notes the division by 100 was checked against prices on the site in GBP and EUR. -
status: 21 of the captured rows wereon-saleand 1sold-out. -
startDate/endDate: ISO 8601 in the venue's local time with its UTC offset;timezonenames the zone. -
cityvssearchCity: where the venue is vs which search found it. -
oneLiner: the organiser's one-line description, null on 11 of the 22 captured rows. -
acquisitionType:purchase, orcompetitionfor a prize draw.
Pro tips
-
Narrow on the site, filter in the Actor.
cities,categoriesand the date range shape what dice.fm returns; keywords, price and on-sale run on the rows. A rare keyword in a big city reads many pages before it fillsmaxItems, so add a category or a date range first. -
Use sub-genres. A tag such as
gig:jazzalso runs on the site. - Batch cities and tags in one run. Each city and tag is its own search, and an event saved under one is not saved again under the next.
-
Mind free events with
maxPrice. A free event has a price of 0, so it passes any ceiling: on the capture,maxPrice: 10would keep 12 of 22 rows, 7 of them free. -
Start small. Keep
maxItems: 20until your downstream code handles every field, including nulloneLinerandpriceFrom, then raise it.
Troubleshooting
- Zero rows: a run whose search reply has the wrong shape or cannot be read ends FAILED. When the site answers with its own empty-result signal, the run ends SUCCEEDED with an empty dataset and a status message saying so. A tag the site does not know returns no events; check the spelling against the list above.
-
Run stops at once: a city name is unknown or ambiguous (the error lists the valid names), a category is not written
group:name, ordateTois beforedateFrom. -
Fewer rows than expected: read the log near its end. It prints one
Skipped N: <reason>line per reason (not on sale,no keyword match,over maxPrice or no price shown) andPushed N row(s).From Python:client.log(run.id).get().
For help, use the Actor's Store page.
Pricing and next steps
For current pricing and details, see the Store page.
To repeat a search, save the input as a task and add an Apify schedule, which runs an Actor or task on a cron expression (0 8 * * * is every day at 8 AM). To react when a run finishes, add a webhook on ACTOR.RUN.SUCCEEDED; its payload carries the run object, including defaultDatasetId.
Which cities or scenes are you planning to track on dice.fm? Drop a comment! 💬
Top comments (0)