Wigmore Hall — Search Concerts by Composer / Artist / Work
Purpose
Return Wigmore Hall (London) concerts whose title, performer line-up, or programme contains a given search term — composer surname (e.g. Beethoven), performer name (e.g. Igor Levit), work title (e.g. Goldberg Variations), or work catalogue number (e.g. BWV1004, Op. 109). Each hit carries the performance title, ISO date/time, programme blurb (composers joined with commas — e.g. "Brahms, Bach, Beethoven and more"), ticket-price summary, canonical concert URL, and Tessitura booking ID. Read-only — never books, reserves, or holds a slot.
When to Use
- "Which upcoming Wigmore concerts feature Schubert?"
- "Is there a Goldberg Variations recital at Wigmore in the next 12 months?"
- Aggregator pulling weekly listings of Beethoven / Schubert / Bach concerts for a music-magazine newsletter.
- Discovering whether a specific performer (e.g.
Yunchan Lim,Lise Davidsen) has any forthcoming Wigmore dates. - Historical look-up across the archive (e.g. "how many Mahler concerts did Wigmore host between 2020 and 2024?").
Workflow
Wigmore Hall publishes a clean, unauthenticated, anti-bot-free JSON API that powers the public /search?term=… page. The API endpoint is GET https://www.wigmore-hall.org.uk/api/v1/search?term={QUERY} — single-segment search across performance titles, performer line-ups, programme subtitles, and per-work composer/title/opus metadata. Use it directly with browse cloud fetch (no --proxies, no --verified, no cookies, no session warmup). The browser path is a fallback only — the /search page is a React SPA that issues the same request, so going through Chrome adds latency without buying anything.
1. Build the request URL
GET https://www.wigmore-hall.org.uk/api/v1/search
?term={url-encoded-query} # required — full-text match
[&page={1-indexed page}] # default 1; page size is fixed at 12
[&startDate=YYYY-MM-DDTHH:mm:ss.000Z] # default today (forthcoming only)
[&endDate=YYYY-MM-DDTHH:mm:ss.000Z] # default unbounded (all future)
Accept: application/json
No Referer, Origin, Cookie, or User-Agent requirements — verified with browse cloud fetch (no proxies) returning 200 in ~250ms.
2. Choose the date window
| Want | startDate | endDate |
|---|---|---|
| Forthcoming only (default) | omit (server defaults to today) | omit |
| Forthcoming + archived (full catalogue) | 1900-01-01T00:00:00.000Z | omit |
| Archived only | 1900-01-01T00:00:00.000Z | today (YYYY-MM-DD) |
| Specific window | window start (ISO Zulu) | window end (ISO Zulu) |
Empirical totals (term=Mozart, captured 2026-05-25): forthcoming = 40, all-time = 838, archived alone = 798. Verified by toggling startDate.
3. Issue the request
curl -sS "https://www.wigmore-hall.org.uk/api/v1/search?term=Schubert" \
| jq '{ total: .totalItems, pages: .totalPages, first: .items[0].node | {title, url, date, subtitleText} }'
…or via the cloud fetch path that mirrors the marketplace agent runtime:
browse cloud fetch "https://www.wigmore-hall.org.uk/api/v1/search?term=Schubert"
Response is a single JSON object: { "items": [...], "totalItems": N, "totalPages": ceil(N/12) }. Page size is hard-coded to 12 server-side — pageSize, perPage, and limit are silently ignored.
4. Decode each items[i].node
Each node has __typename: "Performance" and these fields you'll actually use:
title— performer line-up as one string ("Asmik Grigorian soprano; Lukas Geniušas piano").titleOverrideText— same but with HTML<sub>tags wrapping the instrument labels; strip if you want plain text.subtitleText— programme summary: composer surnames joined with commas plus "and more" if truncated ("Bach, Beethoven, Haydn, Mozart and Schubert"). Treat this as the composer-set summary, NOT a full work list.url— canonical concert path/whats-on/{YYYYMMDDhhmm}. Prefix withhttps://www.wigmore-hall.org.ukfor the absolute URL.date— ISO 8601 with London offset (e.g.2026-06-04T19:30:00+01:00).groupDate—YYYY-MM-DD(no time) for date-grouped UIs.tessituraId— the box-office system's integer ID (string-encoded, e.g."61088"). Useful only if you also call Tessitura.pricesText— HTML-encoded price summary with<br/><sub>+£4 booking fee per transaction</sub>suffix; strip tags or pass through as-is.id— opaque base64 relay node ID (e.g.UGVyZm9ybWFuY2U6Mjc2Njc=); decodes toPerformance:{number}but the number is not the same astessituraId— don't try to derive one from the other.isPriorityBooking— boolean, true when the concert is currently in a Friends/Members priority-booking window.listingImage/listingImageAspect— image URL bundles (multiple breakpoints); uselistingImage.src(400px-wide).
5. Paginate
totalPages = ceil(totalItems / 12). Fetch ?term=…&page=2, …&page=3, … until page > totalPages (which still returns 200 with "items": []). One request per page; ~250ms each from the sandbox. No throttling observed at 5 req/s sustained.
6. Resolve full programme (optional, per-concert)
The search endpoint only exposes a composer-set blurb. If you need the actual list of works (e.g. "Piano Sonata No. 30 in E Op. 109") or the per-work composer attribution, follow url to the SSR concert page https://www.wigmore-hall.org.uk/whats-on/{YYYYMMDDhhmm} and extract from the rendered HTML (look for the #### Programme heading; each composer line is [Composer Name](/artists/{slug}) followed by indented work entries). Pages are server-rendered, no JS needed — browse cloud fetch returns the full markup. Don't try /api/v1/performances/{id} — it 404s (no per-concert JSON endpoint exists).
Browser fallback
Used only when the API namespace is unreachable (unobserved in any of 2026-05-25 testing). Navigate to https://www.wigmore-hall.org.uk/search?term={QUERY} in a bare (no --verified, no --proxies) cloud session, browse wait timeout 3000 after load (the listing widget hydrates ~1–2 s after DOMContentLoaded), then browse get markdown body. The page renders results in chronological calendar groupings (### {Weekday DD Mon YYYY} headings). For archived results, click the ARCHIVED EVENTS ({N}) tab. Don't click BOOK NOW, Book now, or any per-concert price button — those redirect to the Tessitura booking flow. Read-only is the rule.
Site-Specific Gotchas
q=alone returns zero results. Earlier iterations of the search page used?q={query}— the server now requires?term={query}. URLs with onlyq=(noterm=) return{"items":[],"totalItems":0,"totalPages":0}from the API and a blank-result page from the SPA. Always sendterm=, notq=. Sending bothq=andterm=is harmless — server only readsterm=.- Default
startDateis today (server-side, not the URL). Hitting/api/v1/search?term=Mozartwith no date params silently scopes the result set to forthcoming events only — even though the URL contains no date parameter. To get archived events you MUST passstartDate=1900-01-01T00:00:00.000Z(or any sufficiently-old ISO Zulu timestamp). Discovered when the same query returned 40 vs. 838 items depending on whetherstartDatewas set. - Tab-switch on the
/searchpage doesn't change the API call. The five tabs (FORTHCOMING EVENTS,ARCHIVED EVENTS,WATCH,SERIES,PAGES) and their counts are computed client-side from a single super-set response — switching tabs in the UI doesn't trigger a new request. The API endpoint always returns__typename: "Performance"nodes; the Watch / Series / Pages tabs come from a different SSR path and are out of scope for this concert-search skill. contentTypes=Watch|Series|Pagereturns HTTP 500. Don't try to filter content types via the API; the server's GraphQL resolver throws on those values.contentTypes=(empty) is the only value the server accepts; non-empty PascalCase values 500. The other quasi-filter params (watchPageOnly,learningPageOnly,forthcomingPerformancesOnly) are silently accepted but have NO observable effect on the result set (verified by comparing totals across all four variants — all returned 40 forterm=Mozart).- Page size is fixed at 12, server-side.
pageSize,perPage,limit, andcountare all silently ignored — every page returns 12 items max. For large result sets (e.g.Op. 109returns 64 across 6 pages including archive) you'll need to paginate. startDateoverrides IP-geolocation? No — there's no geo gating on this endpoint at all. Same response regardless of source IP. No--proxiesneeded.subtitleTextis a teaser, not the full programme. It's truncated to leading composers + "and more" (e.g.BACH, BEETHOVEN, HAYDN, MOZART AND SCHUBERTfor a 5-composer concert; longer programmes get e.g.BEETHOVEN, CHOPIN, GEORGE XIAOYUAN FU, MESSIAEN AND R SCHUMANN). Don't parse it as a complete composer list. For full programmes, fetch the concert detail page (step 6).- Search hits include performers AND composers AND work titles AND opus numbers. It's a single unified full-text index.
term=Levit(4 hits) finds concerts where Igor Levit performs;term=Beethoven(50+ hits) finds concerts that include any Beethoven work;term=Goldberg%20Variationsfinds the four specific concerts featuring that work. No way to disambiguate composer-vs-performer in the query (e.g. forterm=Mahleryou'll get both performer "Gustav Mahler — not applicable" and composer programmes). When the user supplies a name that is BOTH a composer and a performer (rare but possible with surnames likeMendelssohn), inspect each result'ssubtitleText(composers appear there) vstitle(performers appear there) to classify. - Diacritics matter and must be URL-encoded.
term=Erlk%C3%B6nigreturns 4 hits;term=Erlkonig(no umlaut) returns 0. The index does not fold diacritics. Similarly Schubert →Schubert, Dvořák →Dvo%C5%99%C3%A1k. When the user types ASCII-only, fall back to a substring (e.g.term=Erlkmatches), but you'll get false positives. urlis/whats-on/{YYYYMMDDhhmm}, not/concerts/{id}. The URL slug is derived from the concert start time (Europe/London, 24h). Two concerts on the same day are differentiated by their hour (e.g./whats-on/202605251300lunchtime +/whats-on/202605251930evening). Use theurlfield verbatim — don't try to construct it fromtessituraIdorid.tessituraId≠id. The base64idfield (e.g.UGVyZm9ybWFuY2U6Mjc2Njc=→Performance:27667) is the GraphQL Relay node ID;tessituraId(e.g."61088") is the box-office system ID. Different number spaces — don't map one to the other.- No per-concert API endpoint.
/api/v1/performances/{id}and/api/v1/performances/{tessituraId}both 404. To get full programme detail, you must scrape the SSR/whats-on/{slug}HTML. - Booking-flow URLs end in
/booking/{tessituraId}(e.g./booking/61095). Do not request these — they redirect into the Tessitura booking funnel and may set session/cart state. The search results include them in the "BOOK NOW" anchors; ignore them. - The
/artists/{slug}SSR pages are a viable alternative for exact-composer / exact-performer lookups. Wigmore canonicalises every composer and performer to/artists/{slugified-name}(e.g./artists/ludwig-van-beethoven,/artists/igor-levit). The page is server-rendered and lists concerts grouped by date with tabs labelledEvents (N) | Archived Events (N) | Listen (N) | Watch (N)— the Events count matches the search-API forthcoming total for that name. Useful when you have the canonical slug and want to skip a possibly-ambiguous full-text query. The page only ships the first ~12 entries in the initial SSR; further entries load via JS scrolling — not currently exposed as a JSON endpoint, so paginate via the search API instead. - Cache TTL is 120 s (
Cache-Control: max-age=120, must-revalidate, public). Same query within 2 minutes will return a cached response — fine for repeat reads, but if you're poll-monitoring for new on-sale dates, expect ≤2-minute staleness.
Expected Output
Three distinct outcome shapes — emit the one that matches what the search returned.
// Hit — one or more matching concerts
{
"success": true,
"query": "Schubert",
"scope": "forthcoming",
"total_results": 47,
"concerts": [
{
"title": "Yunchan Lim piano",
"subtitle": "Schubert and Skryabin",
"date_iso": "2026-05-29T13:00:00+01:00",
"date_local": "Fri 29 May 2026 — 1.00pm",
"url": "https://www.wigmore-hall.org.uk/whats-on/202605291300",
"tessitura_id": "61075",
"node_id": "UGVyZm9ybWFuY2U6Mjc2NjA=",
"prices_text": "£60 £53 £43 £33 £18",
"is_priority_booking": false,
"image_url": "https://admin.wigmore-hall.org.uk/images/XlN2nz5b1fOA2qOr-TsWvpsmvMY=/5972/width-400/..."
}
]
}
// No hits
{
"success": true,
"query": "ASCIIComposerWhoDoesntExist",
"scope": "forthcoming",
"total_results": 0,
"concerts": []
}
// Likely-misspelled / diacritic miss (heuristic)
{
"success": false,
"reason": "no_results_possible_diacritic_mismatch",
"query": "Erlkonig",
"hint": "Retry with diacritics URL-encoded — e.g. Erlk%C3%B6nig (Erlkönig). The search index does not fold diacritics.",
"concerts": []
}