docs(research): POST /api/research/events — id-keyed artist live shows - #297
Conversation
…s (chat#1954) Contract-first spec for a new read-only endpoint that returns an artist's live shows keyed on a stable Bandsintown artist id instead of a name string. Name-based show lookup can resolve to a different performer who shares the artist's name; an id lookup makes that impossible. The `country` field is documented with an explicit warning to pair it with `city` when filtering by market, since London and Paris are not unique city names. - add `/api/research/events` path to the research OpenAPI spec - add `ResearchEventsRequest` / `ResearchEventsResponse` schemas - reuse the shared `ResearchErrorResponse` and `ResearchInsufficientCreditsResponse` schemas (400/401/402) - add `api-reference/research/events.mdx` and register it in docs.json nav No 501 is documented: unlike `/api/research/web` this endpoint has no unsupported-data-source path, and the spec should describe only what the API will actually return. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Warning Review limit reached
Next review available in: 22 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe PR documents ChangesResearch Events API
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related issues
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@api-reference/openapi/research.json`:
- Around line 4644-4647: Update the bandsintown_id schema property in the
research API definition to retain its string type while adding a validation
pattern that accepts only one or more decimal digits, so nonnumeric IDs are
rejected.
- Around line 4661-4718: Update the ResearchEventsResponse schema to require the
events property, while preserving its existing array definition and empty-array
behavior when no events match. Add the required-property declaration at the
response object level.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 1f6e8fd0-9cf3-47fc-a3b1-469bf30a3078
📒 Files selected for processing (3)
api-reference/openapi/research.jsonapi-reference/research/events.mdxdocs.json
| "bandsintown_id": { | ||
| "type": "string", | ||
| "description": "Bandsintown numeric artist id — the digits in a Bandsintown artist URL (`bandsintown.com/a/{id}-{slug}`). Identifies the artist exactly, so results cannot drift to a same-named performer.", | ||
| "example": "1590132" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Constrain bandsintown_id to digits.
Line 4645 accepts any string, but Line 4646 defines a numeric Bandsintown artist ID. Generated clients can send invalid values that the endpoint must reject. Add a digits-only pattern.
Proposed fix
"bandsintown_id": {
"type": "string",
+ "pattern": "^[0-9]+$",
"description": "Bandsintown numeric artist id — the digits in a Bandsintown artist URL (`bandsintown.com/a/{id}-{slug}`). Identifies the artist exactly, so results cannot drift to a same-named performer.",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "bandsintown_id": { | |
| "type": "string", | |
| "description": "Bandsintown numeric artist id — the digits in a Bandsintown artist URL (`bandsintown.com/a/{id}-{slug}`). Identifies the artist exactly, so results cannot drift to a same-named performer.", | |
| "example": "1590132" | |
| "bandsintown_id": { | |
| "type": "string", | |
| "pattern": "^[0-9]+$", | |
| "description": "Bandsintown numeric artist id — the digits in a Bandsintown artist URL (`bandsintown.com/a/{id}-{slug}`). Identifies the artist exactly, so results cannot drift to a same-named performer.", | |
| "example": "1590132" |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@api-reference/openapi/research.json` around lines 4644 - 4647, Update the
bandsintown_id schema property in the research API definition to retain its
string type while adding a validation pattern that accepts only one or more
decimal digits, so nonnumeric IDs are rejected.
There was a problem hiding this comment.
All reported issues were addressed across 3 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
The composed 200 example read `"city": "London"` with `"region": "TN"` and `"country": "United Kingdom"` — Mintlify builds the example from per-field examples, so a Nashville state code landed inside the London/Brixton event. Caught by rendering the page locally rather than reading the spec. Region is now exemplified as the empty string, which is what UK venues actually return, and the description carries the "TN" case in prose instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Local preview verification (Chrome DevTools MCP)Ran this branch locally with 🐛 Found and fixed while rendering: incoherent
|
| Check | Result |
|---|---|
Page renders at /api-reference/research/events |
✅ title Artist Events - Recoup |
| Nav placement | ✅ POST Artist Events under Metrics & insights, between Milestones and Similar Artists |
| Method badge | ✅ blue POST (siblings in group are GET) |
| Response code tabs | ✅ exactly 200 · 400 · 401 · 402, no 501, matching the deliberate decision not to copy the sibling's unsupported-source code |
bandsintown_id |
✅ string, required, example "1590132" |
date |
✅ enum<string>, default: upcoming, options upcoming, past, all |
| Empty-result contract | ✅ "An artist with no matching events returns an empty events array, not an error" renders on the Response block |
| All 8 child attributes | ✅ date, venue, city, region, country, ticket_url, sold_out, lineup with descriptions |
region after fix |
✅ Example: "", description carries the US "TN" case in prose |
| Pagination | ✅ ← Milestones / Similar Artists → |
| cURL sample | ✅ targets https://api.recoupable.dev/api/research/events |
Nav placement + response tabs
Full page, child attributes expanded
Screenshots live on the throwaway branch pr-297-verification-assets (commit 89bc25a) purely so they render here, not part of this PR and safe to delete after merge.
One caveat worth stating: Mintlify caches the OpenAPI spec, so the region fix did not hot-reload. The screenshots above are from a restarted dev server confirmed to be serving the patched spec.
Callers should not have to hold a Bandsintown id to ask Recoup a question about their own artist, and publishing a provider id as a request parameter couples the contract to that provider. The endpoint now takes the Recoup `artist_id` and resolves the connected live-events profile server-side. - `ResearchEventsRequest.bandsintown_id` -> `artist_id` (uuid) - add 404 for "artist has no live-events profile connected", documented as explicitly distinct from a 200 with an empty `events` array (connected but not touring). Collapsing those two would let a missing profile read as "no shows", which is the same silent-wrong-answer failure this endpoint exists to prevent. - reword the operation description to describe resolution via the connected profile rather than a provider id Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…server-side Rewrites the endpoint to the contract in recoupable/docs#297. Callers should not have to hold a Bandsintown id to ask Recoup about their own artist — and the sandboxed agent that consumes this could not obtain one anyway, since resolving it needs a credential buildRecoupExecEnv deliberately withholds. The request now takes `artist_id` and the provider id is resolved from that artist's connected socials. - add lib/research/getArtistBandsintownId.ts — parses the numeric id out of a connected bandsintown profile URL. Case-insensitive because profile_url is lowercased by a DB trigger, and tolerant of a missing www. and of trailing query strings. - validator takes `artist_id` (uuid) and carries accountId + orgId through - handler scopes the roster lookup via getArtists(accountId, orgId) before reading the artist's socials. Without this any authenticated account could read any artist's connected profile and events. - 404 when the artist has no bandsintown profile, with the exact URL format and a link to the field that accepts it Two negative cases stay deliberately distinct: no profile connected is a 404, while connected-but-not-touring is a 200 with an empty array. Collapsing them would let a missing profile read as "this artist has no shows" — the same silent-wrong-answer failure this endpoint exists to prevent. Both are tested. 605 tests pass across the research, apify and artists suites. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ample The 404 reused ResearchErrorResponse, whose generic example reads "Missing required parameter: artist" — a validation message. Rendered on the 404 tab that is actively misleading, since the whole point of this 404 is the connection instructions the caller needs to act on. Overrides the example at the response level so the schema stays shared (DRY) while the 404 shows what the API actually returns. Caught by rendering the page, not by reading the spec. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Local preview re-verification,
|
| Check | Result |
|---|---|
| Request body | ✅ artist_id string<uuid> required, example 123694f2-1dab-40b4-8a75-84d39571c0bc |
| Provider removed from params | ✅ no bandsintown_id anywhere in the request schema or cURL sample |
| cURL sample | ✅ {"artist_id": "123694f2-…", "date": "upcoming"} |
| Response code tabs | ✅ 200 · 400 · 401 · 402 · 404 (404 is new; still no 501) |
| 404 example | ✅ renders the real connection-instructions message |
| 404 description | ✅ states it is distinct from a 200 with an empty events array |
| Operation description | ✅ "identified by their Recoup artist_id … Returns 404 when the artist has no live-events profile connected" |
date |
✅ enum<string>, default: upcoming, options upcoming, past, all |
| Anchor | ✅ #body-artist-id (was #body-bandsintown-id) |
region fix from the first round |
✅ still Example: "", no regression |
| Nav placement | ✅ POST Artist Events under Metrics & insights, between Milestones and Similar Artists |
Separately verified that the URL in the 404 message resolves: the artist PATCH body param is profileUrls, and Mintlify kebab-cases it to body-profile-urls, which is present on the live docs page. The link and the instruction are both correct.
Request body, artist_id, no provider id
404 tab showing the real error body
Full page
Screenshots live on the throwaway branch pr-297-verification-assets (commit 5558bc5) purely so they render here, not part of this PR, safe to delete after merge. The earlier round's screenshots were replaced since they showed the superseded bandsintown_id contract.
Same caveat as last time: Mintlify caches the OpenAPI spec, so the 404 example fix did not hot-reload. These screenshots are from a restarted dev server confirmed to serve the patched spec.
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="api-reference/openapi/research.json">
<violation number="1" location="api-reference/openapi/research.json:2364">
P1: Custom agent: **Flag AI Slop and Fabricated Changes**
The 404 example hard-codes a bandsintown-specific error message into a contract that was deliberately redesigned to be provider-agnostic. The description says 'no live-events profile connected' (which is correct), but the example below it names bandsintown explicitly and prescribes its URL format—re-introducing the provider coupling the PR sought to remove. Because the backend hasn't been implemented yet, this example also documents behavior that may not match the actual 404 response. Replace the example with a provider-neutral message that matches the description, or remove the example if the schema shape is already self-explanatory.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| }, | ||
| "example": { | ||
| "status": "error", | ||
| "error": "Error: no bandsintown ID connected to this artist. Please connect the bandsintown ID in this format: bandsintown.com/a/{id}-{slug} Docs here: https://docs.recoupable.dev/api-reference/artists/update#body-profile-urls" |
There was a problem hiding this comment.
P1: Custom agent: Flag AI Slop and Fabricated Changes
The 404 example hard-codes a bandsintown-specific error message into a contract that was deliberately redesigned to be provider-agnostic. The description says 'no live-events profile connected' (which is correct), but the example below it names bandsintown explicitly and prescribes its URL format—re-introducing the provider coupling the PR sought to remove. Because the backend hasn't been implemented yet, this example also documents behavior that may not match the actual 404 response. Replace the example with a provider-neutral message that matches the description, or remove the example if the schema shape is already self-explanatory.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api-reference/openapi/research.json, line 2364:
<comment>The 404 example hard-codes a bandsintown-specific error message into a contract that was deliberately redesigned to be provider-agnostic. The description says 'no live-events profile connected' (which is correct), but the example below it names bandsintown explicitly and prescribes its URL format—re-introducing the provider coupling the PR sought to remove. Because the backend hasn't been implemented yet, this example also documents behavior that may not match the actual 404 response. Replace the example with a provider-neutral message that matches the description, or remove the example if the schema shape is already self-explanatory.</comment>
<file context>
@@ -2358,6 +2358,10 @@
+ },
+ "example": {
+ "status": "error",
+ "error": "Error: no bandsintown ID connected to this artist. Please connect the bandsintown ID in this format: bandsintown.com/a/{id}-{slug} Docs here: https://docs.recoupable.dev/api-reference/artists/update#body-profile-urls"
}
}
</file context>
Reads less like generated copy. Only the sentences authored for this endpoint are changed; the shared 401 and 402 boilerplate keeps its em dash so this endpoint still matches the 24 sibling occurrences of the same string on main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
POST /api/research/events returns a 404 telling callers to connect a bandsintown URL at this field, but BANDSINTOWN was not in the recognized-keys list, so the instruction pointed at a page that did not mention it. Adds it to the recognized keys and the example, and states the URL form the events endpoint parses. The API already accepts arbitrary keys (profileUrls is validated as z.record(z.string(), z.string())), so this documents existing behaviour rather than requesting new behaviour. See the PR discussion for a related defect in updateArtistSocials that this does not fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| }, | ||
| "/api/research/events": { | ||
| "post": { | ||
| "description": "List a Recoup artist's live shows. Returns one row per event with venue, city, country, ticket link, and lineup. The artist is identified by their Recoup `artist_id` and resolved through the live-events profile connected to that artist, so the lookup cannot return a different performer who happens to share the artist's name. Use this instead of web search when you need shows for a specific artist. Returns 404 when the artist has no live-events profile connected.", |
There was a problem hiding this comment.
KISS
| "description": "List a Recoup artist's live shows. Returns one row per event with venue, city, country, ticket link, and lineup. The artist is identified by their Recoup `artist_id` and resolved through the live-events profile connected to that artist, so the lookup cannot return a different performer who happens to share the artist's name. Use this instead of web search when you need shows for a specific artist. Returns 404 when the artist has no live-events profile connected.", | |
| "description": "List an artist's live shows. Returns one row per event with venue, city, country, ticket link, and lineup.", |
Per review on #297. The field-level docs already carry the artist_id resolution and 404 behaviour, so repeating them in the operation description was redundant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review applied + bot triageYour comment: KISS on the operation descriptionApplied verbatim in Agreed. The Heads up: your review is still in pending state, so that comment is only visible to you until you submit it. I could read it through the API but nobody else can see it yet. cubic P1: "404 example hard-codes a bandsintown-specific message into a provider-agnostic contract"Valid observation, but intentional, so not fixing. This is a real tension and the bot spotted it correctly. The request contract is deliberately provider-agnostic ( If we swap providers later, the request contract survives unchanged, which was the point. The error string changes. That is a much cheaper migration than a breaking parameter change. Stale findings: three comments target a property that no longer existsCodeRabbit and cubic both flagged Worth noting the underlying point was already honoured in the replacement: cubic P3:
|
Same paragraph the BANDSINTOWN key was added to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Local preview verification, round 3 (final state)Restarted Events page, shortened descriptionThe KISS suggestion applied. The page now opens with what the endpoint does instead of a five sentence paragraph:
Artists page, BANDSINTOWN added to
|
| Check | Result |
|---|---|
Anchor #body-profile-urls |
✅ resolves and scrolls to the field, so the 404 message's deep link lands correctly |
| Recognized keys | ✅ … THREADS, BANDSINTOWN |
| URL form documented | ✅ bandsintown.com/a/{id}-{slug}, cross-referenced to POST /api/research/events |
| Example object | ✅ includes "BANDSINTOWN": "https://www.bandsintown.com/a/1590132-loreen" |
| Em dash in that paragraph | ✅ removed (case-sensitively, so lowercase keys…) |
Round by round
| Round | Outcome |
|---|---|
| 1 | Found the region: "TN" example on a London venue |
| 2 | Found the 404 rendering the shared schema's "Missing required parameter" example |
| 3 | Clean, no new defects |
Known gap, not fixed here
Documenting BANDSINTOWN does not make the write path idempotent. getSocialPlatformByLink has no bandsintown branch, so it returns "NONE" for those URLs, and updateArtistSocials matches an existing social by getSocialPlatformByLink(url) === type, which can never equal "BANDSINTOWN". First submit inserts; every re-submit inserts again rather than replacing, producing duplicate socials. This affects any platform outside that hardcoded list. Tracked for a separate api change, flagged here so it is not mistaken for something this PR fixed.
Screenshots live on the throwaway branch pr-297-verification-assets (commit 63256ea), not part of this PR, safe to delete after merge. Earlier rounds' images were replaced since they showed superseded contracts.
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| "profileUrls": { | ||
| "type": "object", | ||
| "description": "Map of uppercase platform identifier to social profile URL. Each entry replaces the existing social for that platform; platforms not included are preserved. Recognized keys: SPOTIFY, INSTAGRAM, TIKTOK, TWITTER, YOUTUBE, APPLE, FACEBOOK, THREADS. Keys are matched case-sensitively — lowercase keys will create duplicate socials instead of replacing the existing entry.", | ||
| "description": "Map of uppercase platform identifier to social profile URL. Each entry replaces the existing social for that platform; platforms not included are preserved. Recognized keys: SPOTIFY, INSTAGRAM, TIKTOK, TWITTER, YOUTUBE, APPLE, FACEBOOK, THREADS, BANDSINTOWN. A BANDSINTOWN URL takes the form `bandsintown.com/a/{id}-{slug}` and is what `POST /api/research/events` resolves an artist's live shows through. Keys are matched case-sensitively — lowercase keys will create duplicate socials instead of replacing the existing entry.", |
There was a problem hiding this comment.
OCP / KISS / YAGNI
- actual: new inline "BANDSINTOWN URL takes the form" section in the profile profileUrls description.
- required: dedicated example to show all profile url examples outside of the profileUrls object.
| "description": "Map of uppercase platform identifier to social profile URL. Each entry replaces the existing social for that platform; platforms not included are preserved. Recognized keys: SPOTIFY, INSTAGRAM, TIKTOK, TWITTER, YOUTUBE, APPLE, FACEBOOK, THREADS, BANDSINTOWN. A BANDSINTOWN URL takes the form `bandsintown.com/a/{id}-{slug}` and is what `POST /api/research/events` resolves an artist's live shows through. Keys are matched case-sensitively — lowercase keys will create duplicate socials instead of replacing the existing entry.", | |
| "description": "Map of uppercase platform identifier to social profile URL. Each entry replaces the existing social for that platform; platforms not included are preserved. Recognized keys: SPOTIFY, INSTAGRAM, TIKTOK, TWITTER, YOUTUBE, APPLE, FACEBOOK, THREADS, BANDSINTOWN. Keys are matched case-sensitively — lowercase keys will create duplicate socials instead of replacing the existing entry.", |
… prose Per review on #297 (OCP / KISS / YAGNI). The description no longer explains the BANDSINTOWN URL shape inline; the example now demonstrates the shape for every recognized key instead, so adding a platform later means adding one example line rather than another sentence of prose. All nine keys are the same artist with real URLs, verified rather than invented: Spotify 1Xyo4u8uXC1ZmMpatF05PJ resolves to The Weeknd via the Spotify API, bandsintown.com/a/1371750-the-weeknd and the Apple artist id 479756766 both resolve. A mixed-artist example is how the earlier London/"TN" defect on this PR happened. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Per cubic review on #297. Reverses my earlier call to skip this. I judged it in isolation and said required adds no safety. Checked against the file instead: ResearchErrorResponse and ResearchInsufficientCreditsResponse, both referenced by this endpoint, declare required, and 27 of 50 *Response schemas here do. It also reinforces the contract this endpoint cares about most. `events` is always present and is empty when nothing matches, rather than being omitted, so declaring it required is the accurate description, not just a convention. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Correction to my earlier triageEarlier I declined the "make I judged it in isolation instead of against the file. Checking properly:
Both schemas this endpoint references use it, so omitting it made this endpoint the outlier. There is also a substantive argument I missed. This endpoint's central contract is that
|
…st_id (#826) * feat(research): POST /api/research/events — id-keyed artist live shows (chat#1954) Implements the contract specified in recoupable/docs#297. Looking shows up by artist *name* can resolve to a different, more search-prominent performer who shares that name. This endpoint takes a numeric Bandsintown artist id instead, so the result set cannot drift to the wrong artist. The validator constrains `bandsintown_id` to digits for the same reason: accepting free text would quietly reintroduce the ambiguity the endpoint exists to remove. - lib/apify/bandsintown/fetchBandsintownEvents.ts — actor call + normalization - lib/research/validatePostResearchEventsRequest.ts — auth, body, credit gate - lib/research/ensureEventsResearchCredits.ts — 1 credit, priced like web search - lib/research/postResearchEventsHandler.ts — handler - app/api/research/events/route.ts — thin route, maxDuration 60 Calls Apify synchronously via `.call()`, following fetchSpotifyAlbumPlayCounts rather than the `.start()` + webhook scrapers: this is a read-only fetch whose caller needs the data in its response and which persists nothing. Measured p95 5.7s against the 60s route budget (N=12, concurrency 6). An artist with no events returns 200 with an empty array, not a 404 — "not touring" is a valid answer to the question asked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(research): key events on Recoup artist_id, resolve provider server-side Rewrites the endpoint to the contract in recoupable/docs#297. Callers should not have to hold a Bandsintown id to ask Recoup about their own artist — and the sandboxed agent that consumes this could not obtain one anyway, since resolving it needs a credential buildRecoupExecEnv deliberately withholds. The request now takes `artist_id` and the provider id is resolved from that artist's connected socials. - add lib/research/getArtistBandsintownId.ts — parses the numeric id out of a connected bandsintown profile URL. Case-insensitive because profile_url is lowercased by a DB trigger, and tolerant of a missing www. and of trailing query strings. - validator takes `artist_id` (uuid) and carries accountId + orgId through - handler scopes the roster lookup via getArtists(accountId, orgId) before reading the artist's socials. Without this any authenticated account could read any artist's connected profile and events. - 404 when the artist has no bandsintown profile, with the exact URL format and a link to the field that accepts it Two negative cases stay deliberately distinct: no profile connected is a 404, while connected-but-not-touring is a 200 with an empty array. Collapsing them would let a missing profile read as "this artist has no shows" — the same silent-wrong-answer failure this endpoint exists to prevent. Both are tested. 605 tests pass across the research, apify and artists suites. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(research): name the field when artist_id is missing Found by testing the preview deployment rather than the unit suite. Omitting artist_id returned Zod's default "Invalid input: expected string, received undefined", which never tells the caller which field is missing. Supplying a non-uuid returned the helpful message, so the gap only showed on the omitted-field path. Adds the type-level error message and a test asserting the response names the field. Note the same gap exists on validatePostResearchWebRequest (`query`), which is untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>








Contract for a new read-only endpoint returning a Recoup artist's live shows.
Tracking issue: recoupable/chat#1954
Merge order: this PR first, then recoupable/api#826, the implementation built against this contract (documentation-driven development).
Why id-keyed
Name-based show lookup can resolve to a different performer sharing the artist's name. That is not hypothetical: it put a wrong artist's concert into a customer-facing report on 2026-08-10. Resolving through the artist's connected profile makes that failure structurally impossible.
What's in the spec
POST /api/research/events, body{ artist_id, date? },dateone ofupcoming(default) /past/all.{ status, events[] }withdate,venue,city,region,country,ticket_url,sold_out,lineup[].404and200 []are documented as different answers, and the 404 description says so explicitly:eventsarrayCollapsing them would let a missing profile read as "no shows".
countrycarries a warning to pair it withcitywhen filtering by market, since London and Paris are not unique city names.lineup[]is documented as the queried artist first, so a shared bill is detectable.Conventions followed
ResearchErrorResponse(400/401/404) andResearchInsufficientCreditsResponse(402) schemas.docs.jsonnav under Metrics & insights, next tocareerandmilestones./api/research/webdocuments one, but this endpoint has no unsupported-source path and the spec should describe only what the API returns.Verification
Rendered locally with
mint devand inspected through Chrome DevTools MCP, see the verification comment below for screenshots. That caught a defect invisible in the raw JSON: the composed 200 example read"city": "London"with"region": "TN", because Mintlify builds examples from per-field values. Fixed in302338d.Spec parses;
docs.jsonparses; nav entry resolves. The spec file does not survive ajson.load→json.dumpsround-trip byte-for-byte (key ordering differs at line 2705), so all blocks were inserted via anchored text edits with each anchor asserted unique before substitution.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation