# Recoup: marketing context summaries > Recoup provides AI transformation for music funds and rightsholders: AI strategy, custom systems, and team enablement. The platform, Skills, and API are additional ways to use Recoup's technology. Scope: this file opens with the marketing page summaries used by the public content index, then carries the complete platform documentation, page by page, as served at each page's Source URL. The summaries are not the full text of the website, blog, or playbook. For authoritative detail on those, read the canonical page or use the content reading endpoint with an ID from search. ## Recoup: AI transformation for music rightsholders Content ID: page:/ Source: https://recoupable.dev/ [New: Catalog Skills V2 is here](https://recoupable.dev/skills) We build AI systems you own and train your team to use them. ## Explore Recoup workflows and tools The workflow cards use illustrative figures and sample catalog entries, not client data. ### [Your data. Connected.](https://recoupable.dev/#services) Catalog · Statements One place to work. ### [Royalty report](https://recoupable.dev/#royalty-example) June close Total royalties: $84,600 - Statements: $84,600 - Receipts: $84,600 - 2 Source differences ### [Your next useful build.](https://recoupable.dev/#services) Made for your business. Strategy · Systems · Training ### [Investment review](https://recoupable.dev/#acquisition-example) Model assumption: 60% Publisher share Financial model · Agreement Signed copy needed. ### [Recoup Skills](https://recoupable.dev/skills) A record label. In a box. Music playbooks for your AI. Research · Create · Release ### [Your catalog](https://recoupable.dev/#tools) Music in context - Northbound — The Still Hours - Late Hours — The Still Hours - Afterglow — June Assembly Artists. Music. The full picture. ## Used by teams at Duetti · Seeker Music · Warner Records · Atlantic Records · Rostrum Records · Fat Beats We help the people who create, own, and invest in music put AI to work. ## HOW WE HELP: Choose the help your team needs. [Get a free audit](https://recoupable.dev/start-project) ### AI advisory We assess your workflows and define a first AI project your team can implement. [Explore AI advisory](https://recoupable.dev/advisory) ### AI transformation We lead implementation with your team, from connected data and custom systems to training and results review. [Explore AI transformation](https://recoupable.dev/services#transformation) ### Custom builds Have a project in mind? We build and integrate it, with documented custom code your company controls. [Explore custom builds](https://recoupable.dev/build) ## CASE STUDIES: What we’ve built. [All case studies](https://recoupable.dev/case-studies) ### Royalty reporting Finance & operations A repeatable workbook comparing royalty statements with receipts, with source records behind flagged differences. [Read case study](https://recoupable.dev/case-studies/royalty-reporting) ### Investment review Music investment A diligence draft assembled from models, notes, and emails for an analyst to check and edit. [Read case study](https://recoupable.dev/case-studies/investment-review) ### Catalog intelligence Catalog & creative Recurring briefs flag period changes, data issues, and questions for the team to investigate. [Read case study](https://recoupable.dev/case-studies/catalog-intelligence) Anonymized client work. Illustrations contain no client data. ## HOW IT WORKS: From first conversation to launch. ### Step 01: Discovery & audit Review your workflows, tools, data, and recurring tasks. ### Step 02: Strategy & roadmap Agree on priorities, scope, price, responsibilities, and success measures. ### Step 03: Build & integration Build, connect, and test the system with your team. ### Step 04: Launch & adoption Train your team, hand over documentation, and review results together. Have a defined project? Start with the build. Ongoing support is scoped and priced separately. ## After the handoff: Built by Recoup. Owned by you. - **Your code.** Custom code in a repository your company controls. - **Your documentation.** How to run, maintain, and connect the system. - **Your team.** Hands-on training for the people running it. Ownership, licenses, hosting, and platform or third-party costs are agreed in scope. Continued support is separate. ## WORK WITH RECOUP: Choose your level of support. Choose your billing: Monthly or Annually. Annual prices show the monthly equivalent, with the full annual charge listed alongside. Save 20% or more annually ### Advisory Your team implements. We provide strategy, planning, and coaching. - Monthly: Starting at $999 / month. USD · Billed monthly. [Explore advisory](https://recoupable.dev/start-project?plan=advisory&billing=monthly) - Annually: Starting at $799 / month. USD · $9,588 billed annually. [Explore advisory](https://recoupable.dev/start-project?plan=advisory&billing=annual) ### Build + Partner We build and integrate the systems, train your team, and support adoption. - Monthly: Starting at $9,999 / month. USD · Billed monthly. [Discuss your build](https://recoupable.dev/start-project?plan=partner&billing=monthly) - Annually: Starting at $7,999 / month. USD · $95,988 billed annually. [Discuss your build](https://recoupable.dev/start-project?plan=partner&billing=annual) Scope and delivery schedule agreed before work begins. [Compare all plans](https://recoupable.dev/pricing) ## RECOUP TOOLS: Use Recoup Platform. Or bring your own AI. Use our software and skills without a consulting engagement. ### Platform: Your music team’s AI workspace. Research artists, draft content, and run recurring work with your artist context in one place. $99 / month. Recoup Platform + music skill pack · USD [Explore Recoup Platform](https://recoupable.dev/platform) ### Open-source skills: Give your agent a music playbook. Open-source playbooks for research, releases, content, and catalogs, installed in a compatible AI agent. [Explore Skills](https://recoupable.dev/skills) ### Developer tools: Build Recoup into your product. Add artist context, research, and content tools to your application through the API, MCP, or CLI. [Explore developer tools](https://recoupable.dev/developers) ## Before we get started. ### What does the free audit include? We review one workflow and identify a practical first step. Bring the task and tools involved. Implementation is scoped and priced separately. ### What will our company own? Your custom code, documentation, and training are included. The agreement defines ownership and licenses; scope identifies platform, hosting, and third-party costs. Code is delivered in a repository you control. ### Can you work with our tools and developers? Yes. We agree on integrations, responsibilities, and handoff with your team before building. ### How will you access our data? We agree on the access, hosting, AI providers, and data handling your workflow needs. No confidential files are needed to request an audit. ### What does a build cost, and what happens after launch? We agree on scope, price, and schedule before starting. Documentation and training are included; ongoing support is scoped separately. [Compare plans.](https://recoupable.dev/pricing) ## Recoup your team’s time. Bring us a workflow that slows you down. We’ll show you where AI can help. [Get a free audit](https://recoupable.dev/start-project) Implementation scoped and priced separately. ## The Recoup blog Content ID: page:/blog Source: https://recoupable.dev/blog Ideas and practical notes on AI in music companies. ## [The Music Executive's Guide to AI Agents: What to Buy, What to Build, What to Ignore](https://recoupable.dev/blog/music-executive-guide-ai-agents) Published: 2026-05-18 A decision framework for music executives evaluating AI: what to buy, what to build, and how to test a useful first workflow. [Read Markdown](https://recoupable.dev/blog/music-executive-guide-ai-agents.md) ## [Is your music catalog earning what you expected?](https://recoupable.dev/blog/ai-agents-for-music-catalog-reporting) Published: 2026-09-27 How Recoup helps catalog teams use AI agents to prepare income comparisons, investigate gaps and build a reporting process their own people can run. [Read Markdown](https://recoupable.dev/blog/ai-agents-for-music-catalog-reporting.md) ## [Your AI lives in one person's head](https://recoupable.dev/blog/your-ai-lives-in-one-persons-head) Published: 2026-09-28 How Recoup helps music companies document, test, and hand over AI workflows so another employee can run them without the builder. [Read Markdown](https://recoupable.dev/blog/your-ai-lives-in-one-persons-head.md) ## [Use AI to bring catalog data into your quarterly report](https://recoupable.dev/blog/automate-royalty-reporting-with-existing-files) Published: 2026-09-14 See how a finance lead used Claude to transfer catalog data into a quarterly report, check totals, and make progress before live integrations were ready. [Read Markdown](https://recoupable.dev/blog/automate-royalty-reporting-with-existing-files.md) ## [Use past investment memos to teach AI how your firm evaluates deals](https://recoupable.dev/blog/teach-ai-with-past-investment-memos) Published: 2026-09-14 We customized Recoup's free Deal Ingestion Skill with a client in one session, using past investment memos to reflect the firm's curves and investment standards. [Read Markdown](https://recoupable.dev/blog/teach-ai-with-past-investment-memos.md) ## [How to roll out AI beyond your first successful team](https://recoupable.dev/blog/roll-out-ai-beyond-first-team) Published: 2026-09-14 Use practical one-on-one sessions to help the next department adopt AI, then turn the requirements teams discover into shared instructions and systems. [Read Markdown](https://recoupable.dev/blog/roll-out-ai-beyond-first-team.md) ## [How to show your board whether AI is paying off](https://recoupable.dev/blog/show-your-board-ai-roi) Published: 2026-09-14 Compare a workflow’s full cost before and after AI. Use a board-reporting table that separates measured results, recalled estimates, and missing evidence. [Read Markdown](https://recoupable.dev/blog/show-your-board-ai-roi.md) ## [Why Artists Need AI Agents, Not AI Tools](https://recoupable.dev/blog/why-artists-need-ai-agents) Published: 2026-05-09 How a connected AI workflow can help musicians organize drafts, context, and review, and how to tell whether it is actually saving effort. [Read Markdown](https://recoupable.dev/blog/why-artists-need-ai-agents.md) ## [How to Create a Month of Content in One Day](https://recoupable.dev/blog/ai-content-creation-musicians) Published: 2026-05-09 A practical batching exercise for turning artist material into a month of planned drafts, with clear steps for review and scheduling. [Read Markdown](https://recoupable.dev/blog/ai-content-creation-musicians.md) ## [The Complete Music Release Strategy for 2026](https://recoupable.dev/blog/music-release-strategy-2026) Published: 2026-05-09 A phase-by-phase music release strategy covering pre-release, launch week, and long-tail promotion with platform-specific tactics for 2026. [Read Markdown](https://recoupable.dev/blog/music-release-strategy-2026.md) ## [AI A&R: How Labels Are Using AI to Find the Next Hit Artist](https://recoupable.dev/blog/ai-ar-artist-discovery) Published: 2026-05-18 How to use AI for sourced artist briefs, catalog comparisons, and an A&R research pipeline your team can evaluate. [Read Markdown](https://recoupable.dev/blog/ai-ar-artist-discovery.md) ## [Your Catalog Is Dying. AI Agents Can Revive It.](https://recoupable.dev/blog/ai-catalog-marketing-passive-revenue) Published: 2026-05-17 How to identify catalog work worth testing, prepare a reviewed campaign, and measure effort separately from revenue outcomes. [Read Markdown](https://recoupable.dev/blog/ai-catalog-marketing-passive-revenue.md) ## [AI for Record Labels: How Smart Labels Are Cutting Costs and Scaling Output](https://recoupable.dev/blog/ai-for-record-labels) Published: 2026-05-12 Where AI can help a label’s marketing team: recurring preparation, content drafts, catalog research, and reporting, with a practical way to evaluate the cost and quality. [Read Markdown](https://recoupable.dev/blog/ai-for-record-labels.md) ## [AI for Music Distributors: Automate the Ops That Are Killing Your Margins](https://recoupable.dev/blog/ai-music-distribution-automation) Published: 2026-05-18 How to evaluate AI for metadata checks, marketing preparation, reporting, and catalog monitoring without losing review and service quality. [Read Markdown](https://recoupable.dev/blog/ai-music-distribution-automation.md) ## [The Best AI Tools for Music Managers in 2026](https://recoupable.dev/blog/ai-music-manager-tools) Published: 2026-05-12 A workflow-based guide to choosing AI tools for artist management: context, data access, approvals, total cost, and useful output. [Read Markdown](https://recoupable.dev/blog/ai-music-manager-tools.md) ## [The ROI of AI Music Marketing: What the Numbers Say](https://recoupable.dev/blog/ai-music-marketing-roi) Published: 2026-05-09 How to compare software, setup, review time, and actual cash savings when evaluating AI for music marketing. [Read Markdown](https://recoupable.dev/blog/ai-music-marketing-roi.md) ## [How AI is Changing Music Marketing](https://recoupable.dev/blog/ai-music-marketing) Published: 2026-03-16 A practical workflow for connecting artist context, content drafts, review, and publishing preparation with AI. [Read Markdown](https://recoupable.dev/blog/ai-music-marketing.md) ## [Why Labels Are Replacing Marketing Teams with AI Agents](https://recoupable.dev/blog/ai-replacing-music-marketing-teams) Published: 2026-05-17 Which marketing tasks can AI assist with, and what should a label measure before changing staffing or spend? [Read Markdown](https://recoupable.dev/blog/ai-replacing-music-marketing-teams.md) ## [How Labels Use AI in 2026: From Catalog Reactivation to Autonomous Marketing](https://recoupable.dev/blog/how-labels-use-ai) Published: 2026-05-12 Five AI workflows a label can evaluate, from catalog research and content localization to artist marketing preparation and performance review. [Read Markdown](https://recoupable.dev/blog/how-labels-use-ai.md) ## [How Much Does AI Music Marketing Actually Cost? A Real Breakdown](https://recoupable.dev/blog/how-much-does-ai-music-marketing-cost) Published: 2026-05-17 The costs to include when budgeting for AI music marketing, from subscriptions and implementation to review and ongoing support. [Read Markdown](https://recoupable.dev/blog/how-much-does-ai-music-marketing-cost.md) ## [The Independent Artist's Marketing Guide: No Label, No Budget, No Problem](https://recoupable.dev/blog/independent-artist-marketing-guide) Published: 2026-05-09 A small-budget music marketing plan with content ideas, a suggested weekly schedule, relevant outreach, and practical ways to test AI. [Read Markdown](https://recoupable.dev/blog/independent-artist-marketing-guide.md) ## [AI for Music Managers: Automate the Busywork, Keep the Relationships](https://recoupable.dev/blog/ai-for-music-managers) Published: 2026-05-19 How managers can evaluate AI for reporting, content drafts, and outreach preparation while keeping ownership of artist relationships. [Read Markdown](https://recoupable.dev/blog/ai-for-music-managers.md) ## [AI Music Marketing: The Complete Guide for 2026](https://recoupable.dev/blog/ai-music-marketing-guide-2026) Published: 2026-05-20 Workflows to test for music marketing, the data and review controls they need, and how artists, managers, and labels can evaluate the results. [Read Markdown](https://recoupable.dev/blog/ai-music-marketing-guide-2026.md) ## [AI Playlist Pitching: How to Get More Placements Without More Hours](https://recoupable.dev/blog/ai-playlist-pitching) Published: 2026-05-16 How to use AI for playlist research, relevant pitch drafts, and response tracking, with checks for fit, approvals, and measurable results. [Read Markdown](https://recoupable.dev/blog/ai-playlist-pitching.md) ## [ChatGPT vs. Music AI Agents: Why Generic AI Falls Short](https://recoupable.dev/blog/chatgpt-vs-music-ai-agents) Published: 2026-05-17 How to compare general-purpose assistants and music-focused workflows by data access, context, recurring tasks, and review controls. [Read Markdown](https://recoupable.dev/blog/chatgpt-vs-music-ai-agents.md) ## [Meta Bought Manus Because Agents Still Break](https://recoupable.dev/blog/meta-bought-manus-agents-break) Published: 2026-01-01 Meta's $2-3B acquisition of Manus signals where AI value is accumulating: not in models, but in orchestration layers that keep agents coherent across real work. [Read Markdown](https://recoupable.dev/blog/meta-bought-manus-agents-break.md) ## [Why Music Labels Are Hiring AI Agents Instead of More Interns](https://recoupable.dev/blog/music-label-ai-agents) Published: 2026-05-18 A practical way for labels to evaluate AI for catalog research, A&R preparation, and artist operations without assuming staffing or revenue gains. [Read Markdown](https://recoupable.dev/blog/music-label-ai-agents.md) ## [Recoup in 2026](https://recoupable.dev/blog/recoup-in-2026) Published: 2026-01-05 In 2026, we’re optimizing Recoup for enterprise label teams: recurring value for every seat, measurable time saved, and clearer outcomes per campaign. [Read Markdown](https://recoupable.dev/blog/recoup-in-2026.md) ## [Sandbox for Record Labels](https://recoupable.dev/blog/sandbox-for-record-labels) Published: 2026-01-26 Use Recoup sandboxes to give your music team's AI agents a file system, documents, and tools for working with artist context. [Read Markdown](https://recoupable.dev/blog/sandbox-for-record-labels.md) ## [Open Labels](https://recoupable.dev/blog/open-labels) Published: 2026-04-22 How Recoup's API and agent tools let you build a programmable record label around your artists, catalog, and music workflows. [Read Markdown](https://recoupable.dev/blog/open-labels.md) ## [Install the Recoup Skills & Plugins Marketplace on Claude Desktop](https://recoupable.dev/blog/install-marketplace-claude-desktop) Published: 2026-05-15 Install the Recoup Skills & Plugins Marketplace in Claude Desktop, add Recoup skills, and connect your music workspace. [Read Markdown](https://recoupable.dev/blog/install-marketplace-claude-desktop.md) ## [Bring your own agent](https://recoupable.dev/blog/bring-your-own-agent) Published: 2026-05-19 Connect your own AI agent to Recoup with its API, CLI, and MCP server, and bring music workflows into the tools you already use. [Read Markdown](https://recoupable.dev/blog/bring-your-own-agent.md) ## The AI Music Marketing Playbook Content ID: page:/playbook/download Source: https://recoupable.dev/playbook/download # Choose the work worth improving Source: https://recoupable.dev/playbook/download#choose-the-work Start with a recurring task, a clear owner, and a useful result. ## Map the work before choosing a tool List the work that repeats around an artist or release: research, content preparation, approval, distribution, outreach, and reporting. Record who does it, which information they need, and how long it takes. Choose a first task with a repeatable input and an output someone can review. Drafting a release brief from approved materials is easier to assess than asking an agent to ‘grow the artist.’ ## Tools, agents, and the full workflow A tool helps with a step, such as drafting a caption. An agent can coordinate several steps when it has the right instructions, access, and integrations. Neither removes the need to decide what good work looks like. Compare the complete cost: setup, software, usage, review, and maintenance. Measure the time saved on your own work instead of assuming an agent replaces every hour someone spends on the task. # Build a repeatable content process Source: https://recoupable.dev/playbook/download#content Create a batch, review it together, and use what you learn next time. ## Prepare the artist context Collect an approved biography, music links, release details, audience notes, examples of the artist’s voice, and subjects to avoid. Keep these in one place so the next brief starts with the same context. 1. Choose the campaign goal and channels. 2. Ask for several angles and formats using the approved artist material. 3. Select the strongest ideas and check facts, voice, and rights. 4. Put approved work into your calendar or connected publishing tool. 5. Review results and use the findings in the next batch. ## Four prompts to start with ### Instagram carousel Create a 7-slide Instagram carousel for [ARTIST NAME] about [TOPIC]. Audience: [DEMOGRAPHIC]. Tone: [authentic/hype/vulnerable/educational]. Include a hook on slide 1, value on slides 2–6, and a CTA on slide 7. Reference recent track [SONG NAME]. Use only the facts in the attached artist brief. ### Short-form video Write a 30-second video script for [ARTIST NAME]. Hook in the first 3 seconds: [PATTERN INTERRUPT]. Middle: [STORY/REVEAL]. End: [CTA]. Include on-screen text suggestions. Separate the script from footage or music we still need permission to use. ### Release announcement Create 5 variations of a release announcement for [SONG/ALBUM NAME] by [ARTIST NAME], releasing on [DATE]. Try these angles: emotional, high-energy, behind-the-scenes, fan-focused, and teaser. Keep the date, title, and links exactly as supplied. ### Thread Write a 5-post thread from [ARTIST NAME]’s perspective about [TOPIC]. Start with a strong hook. Posts 2–4 should tell a story using the attached source material. The last post should direct readers to [LINK/ACTION]. Use first-person, conversational language. ## Improve the next batch Look at which formats earned useful responses, what fans asked about, and which posts underperformed. Turn those findings into explicit changes to the next brief. Keep the artist’s judgment in the loop; engagement alone does not define their voice. # Research the audience Source: https://recoupable.dev/playbook/download#audience Turn available audience information into a brief your team can act on. ## Start with information you can access Bring together artist analytics, social reports, campaign results, and research on comparable artists. Access varies by platform and account; ask the agent to identify missing information rather than fill it with guesses. - Where listeners and engaged fans are located. - Which platforms and formats earn a response. - Themes in comments, questions, and fan conversations. - Comparable artists, relevant curators, and potential collaborators. ## Make the output useful Ask for an audience brief with sources, observations, and proposed actions. Keep measured facts separate from hypotheses. A suggested audience to test is different from an audience you have already reached. ### Audience brief Using these reports, summarize our current audience and three opportunities to investigate. For each finding, cite its source and reporting period. Separate observed data from hypotheses. List the missing information that would change your recommendation. # Plan the whole release Source: https://recoupable.dev/playbook/download#release Organize pre-release, launch, and follow-up work around one calendar. ## Before release Confirm the release date, approved artwork, credits, links, and campaign goal. Work backward to plan teasers, the announcement, press materials, creator briefs, and any pre-save campaign. - Draft channel-specific content from the same release brief. - Research relevant curators, publications, and collaborators. - Prepare outreach for someone to review and send. - Assign an owner and approval deadline to every deliverable. ## Release week Check that the live links and release information are correct. Prepare launch posts, emails, and responses to common questions. Use the calendar to coordinate timing across the team. Ask for a concise daily report on available campaign and listening data. Flag missing feeds or delayed reporting so the team understands what the report can actually show. ## After release Keep useful promotion going with live clips, fan reactions you have permission to share, behind-the-scenes material, and new stories about the music. Review what worked before planning the next release. Let observed results shape the cadence. A fixed posting schedule is a starting point to test, not a guarantee of growth. # Manage work across a roster Source: https://recoupable.dev/playbook/download#roster Keep each artist’s context distinct while sharing useful methods. ## One method, different artist context A shared workflow can serve several artists, but each needs their own approved information, voice, release calendar, permissions, and goals. Keep those boundaries clear when connecting accounts and files. 1. Review an overnight or weekly brief across the roster. 2. Open the items that need a decision or approval. 3. Review draft content in each artist’s context. 4. Assign opportunities and follow-up work to named owners. 5. Update the shared workflow when the team finds a better method. ## Connect the tools you already use Distribution services, analytics tools, design apps, project trackers, and email platforms all have different access and integration options. Decide which system owns each record before connecting them. Start with the handoff that causes the most repeated work. A dependable connection between two tools is more useful than a broad setup nobody can maintain. # Put the catalog to work Source: https://recoupable.dev/playbook/download#catalog Apply the same discipline to older releases, reporting, and label operations. ## Find useful starting points - Catalog marketing: draft new campaign angles around existing music. - Artist research: bring comparable releases and audience signals into a sourced brief. - Reporting: prepare a consistent view of the data the team can access. - Artist services: reuse approved research, content, and release methods across the roster. ## Expand after the first workflow works Begin with a small set of artists or releases and a clear review process. Test the workflow with the people who will use it. Expand after you can repeat the result and understand the exceptions. Track preparation time, corrections, and whether the team can run the workflow again. Use what you learn to decide which catalog or department to bring in next. # Start with one useful build Source: https://recoupable.dev/playbook/download#first-build Turn an idea into a small project the team can evaluate. ## Your first steps 1. Choose a recurring task and the person responsible for it. 2. Collect the inputs and one example of an acceptable output. 3. Write down the checks the reviewer will make. 4. Run the workflow on a small set of real tasks. 5. Compare the result, review effort, and time with the current process. 6. Document how to run it, then decide what to improve or expand. ## Bring the hard part to Recoup If the challenge spans your data, existing tools, and team habits, Recoup can help scope and build the system. Bring a workflow or walk us through how the team works. We will discuss a useful first project and how to assess it. ## The Recoup API Content ID: page:/docs/api-reference Source: https://recoupable.dev/docs/api-reference Artist data, research, content creation, and the tools to connect them. Find the endpoint you need and start building. - [Get Artists](https://recoupable.dev/docs/api-reference/artists/list) — GET /api/artists - [Get Artist Profile](https://recoupable.dev/docs/api-reference/artists/profile) — GET /api/artists/{id}/profile - [Create Artist](https://recoupable.dev/docs/api-reference/artists/create) — POST /api/artists - [Update Artist](https://recoupable.dev/docs/api-reference/artists/update) — PATCH /api/artists/{id} - [Pin Artist](https://recoupable.dev/docs/api-reference/artists/pin) — PATCH /api/artists/{id} - [Unpin Artist](https://recoupable.dev/docs/api-reference/artists/unpin) — PATCH /api/artists/{id} - [Delete Artist](https://recoupable.dev/docs/api-reference/artists/delete) — DELETE /api/artists/{id} - [Get Artist Socials](https://recoupable.dev/docs/api-reference/artists/socials) — GET /api/artists/{id}/socials - [Delete Artist Social](https://recoupable.dev/docs/api-reference/artists/socials-delete) — DELETE /api/artists/{id}/socials/{socialId} - [Get Artist Fans](https://recoupable.dev/docs/api-reference/fans/get) — GET /api/artists/{id}/fans - [Stream Chat](https://recoupable.dev/docs/api-reference/chat/workflow) — POST /api/chat - [Resume Chat Stream](https://recoupable.dev/docs/api-reference/chat/workflow-stream) — GET /api/chat/{chatId}/stream - [Start Chat Run](https://recoupable.dev/docs/api-reference/chat/runs) — POST /api/chat/runs - [Get Chat Run Status](https://recoupable.dev/docs/api-reference/chat/runs-status) — GET /api/chat/runs/{runId} - [Stop Chat](https://recoupable.dev/docs/api-reference/chat/stop) — POST /api/chat/{chatId}/stop - [Create Chat](https://recoupable.dev/docs/api-reference/chat/create) — POST /api/chats - [Get Chats](https://recoupable.dev/docs/api-reference/chat/chats) — GET /api/chats - [Get Chat Artist](https://recoupable.dev/docs/api-reference/chat/artist) — GET /api/chats/{id}/artist - [Update Chat](https://recoupable.dev/docs/api-reference/chat/update) — PATCH /api/chats - [Delete Chat](https://recoupable.dev/docs/api-reference/chat/delete) — DELETE /api/chats - [Compact Chats](https://recoupable.dev/docs/api-reference/chat/compact) — POST /api/chats/compact - [List Models](https://recoupable.dev/docs/api-reference/ai/models) — GET /api/ai/models - [Get Chat Messages](https://recoupable.dev/docs/api-reference/chat/messages) — GET /api/chats/{id}/messages - [Copy Chat Messages](https://recoupable.dev/docs/api-reference/chat/messages-copy) — POST /api/chats/{id}/messages/copy - [Delete Trailing Chat Messages](https://recoupable.dev/docs/api-reference/chat/messages-trailing-delete) — DELETE /api/chats/{id}/messages/trailing - [Research Search](https://recoupable.dev/docs/api-reference/research/search) — GET /api/research - [Lookup by URL](https://recoupable.dev/docs/api-reference/research/lookup) — GET /api/research/lookup - [Artist Profile](https://recoupable.dev/docs/api-reference/research/profile) — GET /api/research/profile - [Platform Metrics](https://recoupable.dev/docs/api-reference/research/metrics) — GET /api/research/metrics - [Audience Demographics](https://recoupable.dev/docs/api-reference/research/audience) — GET /api/research/audience - [Insights Activities](https://recoupable.dev/docs/api-reference/research/insights) — GET /api/research/insights - [Career Timeline](https://recoupable.dev/docs/api-reference/research/career) — GET /api/research/career - [Milestones](https://recoupable.dev/docs/api-reference/research/milestones) — GET /api/research/milestones - [Artist Events](https://recoupable.dev/docs/api-reference/research/events) — POST /api/research/events - [Similar Artists](https://recoupable.dev/docs/api-reference/research/similar) — GET /api/research/similar - [Social URLs](https://recoupable.dev/docs/api-reference/research/urls) — GET /api/research/urls - [Albums](https://recoupable.dev/docs/api-reference/research/albums) — GET /api/research/albums - [Tracks](https://recoupable.dev/docs/api-reference/research/tracks) — GET /api/research/tracks - [Track](https://recoupable.dev/docs/api-reference/research/track) — GET /api/research/track - [Track Stats](https://recoupable.dev/docs/api-reference/research/track-stats) — GET /api/research/track/stats - [Track measurements](https://recoupable.dev/docs/api-reference/research/measurements) — GET /api/research/tracks/{id}/measurements - [Album measurements](https://recoupable.dev/docs/api-reference/research/album-measurements) — GET /api/research/albums/{id}/measurements - [Create measurement job](https://recoupable.dev/docs/api-reference/research/measurement-jobs) — POST /api/research/measurement-jobs - [Playlist Placements](https://recoupable.dev/docs/api-reference/research/playlists) — GET /api/research/playlists - [Track Playlists](https://recoupable.dev/docs/api-reference/research/track-playlists) — GET /api/research/track/playlists - [Web Search](https://recoupable.dev/docs/api-reference/research/web) — POST /api/research/web - [Deep Research](https://recoupable.dev/docs/api-reference/research/deep) — POST /api/research/deep - [People Search](https://recoupable.dev/docs/api-reference/research/people) — POST /api/research/people - [Extract URL](https://recoupable.dev/docs/api-reference/research/extract) — POST /api/research/extract - [Enrich](https://recoupable.dev/docs/api-reference/research/enrich) — POST /api/research/enrich - [Get Songs](https://recoupable.dev/docs/api-reference/songs/songs) — GET /api/songs - [Create Songs](https://recoupable.dev/docs/api-reference/songs/create) — POST /api/songs - [Analyze Songs](https://recoupable.dev/docs/api-reference/songs/analyze) — POST /api/songs/analyze - [List Analyze Presets](https://recoupable.dev/docs/api-reference/songs/analyze-presets) — GET /api/songs/analyze/presets - [Get Catalogs](https://recoupable.dev/docs/api-reference/songs/catalogs) — GET /api/accounts/{id}/catalogs - [Create Catalog](https://recoupable.dev/docs/api-reference/songs/catalogs-create) — POST /api/catalogs - [Rename Catalog](https://recoupable.dev/docs/api-reference/songs/catalog-rename) — PATCH /api/catalogs/{catalogId} - [Delete Catalog](https://recoupable.dev/docs/api-reference/songs/catalog-delete) — DELETE /api/catalogs/{catalogId} - [Run Valuation](https://recoupable.dev/docs/api-reference/songs/valuation-run) — POST /api/valuation - [Get Runs](https://recoupable.dev/docs/api-reference/songs/runs) — GET /api/runs - [Get Catalog Measurements](https://recoupable.dev/docs/api-reference/songs/catalog-measurements) — GET /api/catalogs/{catalogId}/measurements - [Catalog Playcount History](https://recoupable.dev/docs/api-reference/songs/catalog-playcount-history) — GET /api/catalogs/{catalogId}/playcount-history - [Get Catalog Valuations](https://recoupable.dev/docs/api-reference/songs/catalog-valuations) — GET /api/catalogs/{catalogId}/valuations - [Get Catalog Songs](https://recoupable.dev/docs/api-reference/songs/catalog-songs) — GET /api/catalogs/songs - [Add Catalog Songs](https://recoupable.dev/docs/api-reference/songs/catalog-songs-add) — POST /api/catalogs/songs - [Remove Catalog Songs](https://recoupable.dev/docs/api-reference/songs/catalog-songs-delete) — DELETE /api/catalogs/songs - [Daily Catalog Streams](https://recoupable.dev/docs/api-reference/songs/catalog-streams) — GET /api/catalogs/{catalogId}/streams - [Catalog Stream Tracking](https://recoupable.dev/docs/api-reference/songs/catalog-stream-tracking) — GET /api/catalogs/{catalogId}/stream-tracking - [Update Catalog Stream Tracking](https://recoupable.dev/docs/api-reference/songs/update-catalog-stream-tracking) — POST /api/catalogs/{catalogId}/stream-tracking - [Context Engine](https://recoupable.dev/docs/api-reference/context/operate) — POST /api/context - [Start guest context](https://recoupable.dev/docs/api-reference/context/guest-start) — POST /api/context/guest - [Read guest context](https://recoupable.dev/docs/api-reference/context/guest-read) — GET /api/context/guest - [Claim guest context](https://recoupable.dev/docs/api-reference/context/guest-claim) — POST /api/context/guest/claim - [Create Content](https://recoupable.dev/docs/api-reference/content/create) — POST /api/content/create - [Generate Image](https://recoupable.dev/docs/api-reference/content/generate-image) — POST /api/content/image - [Generate Video](https://recoupable.dev/docs/api-reference/content/generate-video) — POST /api/content/video - [Generate Caption](https://recoupable.dev/docs/api-reference/content/generate-caption) — POST /api/content/caption - [Transcribe Audio](https://recoupable.dev/docs/api-reference/content/transcribe-audio) — POST /api/content/transcribe - [Edit Content](https://recoupable.dev/docs/api-reference/content/edit) — PATCH /api/content - [Upscale Content](https://recoupable.dev/docs/api-reference/content/upscale) — POST /api/content/upscale - [Analyze Video](https://recoupable.dev/docs/api-reference/content/analyze-video) — POST /api/content/analyze - [List Templates](https://recoupable.dev/docs/api-reference/content/templates) — GET /api/content/templates - [Get Template Detail](https://recoupable.dev/docs/api-reference/content/template-detail) — GET /api/content/templates/{id} - [Validate Artist](https://recoupable.dev/docs/api-reference/content/validate) — GET /api/content/validate - [Estimate Cost](https://recoupable.dev/docs/api-reference/content/estimate) — GET /api/content/estimate - [Generate Image](https://recoupable.dev/docs/api-reference/image/generation) — GET /api/image/generate - [Transcribe Audio](https://recoupable.dev/docs/api-reference/transcribe/audio) — POST /api/transcribe - [Generate lipsync (release pending)](https://recoupable.dev/docs/api-reference/content/lipsync) — POST /api/content/lipsync - [Generate Music](https://recoupable.dev/docs/api-reference/music/generate) — POST /api/music - [List Music Generations](https://recoupable.dev/docs/api-reference/music/list) — GET /api/music - [Get Music Generation](https://recoupable.dev/docs/api-reference/music/get) — GET /api/music/{generationId} - [Get Artist Posts](https://recoupable.dev/docs/api-reference/posts/get) — GET /api/artists/{id}/posts - [Get Comments](https://recoupable.dev/docs/api-reference/comments/get) — GET /api/comments - [Social Scrape](https://recoupable.dev/docs/api-reference/social/scrape) — POST /api/socials/{id}/scrape - [Scrape Artist Socials](https://recoupable.dev/docs/api-reference/artist/socials-scrape) — POST /api/artist/socials/scrape - [Search](https://recoupable.dev/docs/api-reference/spotify/search) — GET /api/spotify/search - [Get Artist](https://recoupable.dev/docs/api-reference/spotify/artist) — GET /api/spotify/artist/ - [Get Artist Albums](https://recoupable.dev/docs/api-reference/spotify/artist-albums) — GET /api/spotify/artist/albums - [Get Artist Top Tracks](https://recoupable.dev/docs/api-reference/spotify/artist-top-tracks) — GET /api/spotify/artist/topTracks - [Get Album](https://recoupable.dev/docs/api-reference/spotify/album) — GET /api/spotify/album - [Songs by ISRC](https://recoupable.dev/docs/api-reference/apple/songs) — GET /api/apple/songs - [Scraper Results](https://recoupable.dev/docs/api-reference/apify/scraper) — GET /api/apify/runs/{runId} - [Agent Signup](https://recoupable.dev/docs/api-reference/agents/signup) — POST /api/agents/signup - [Verify Agent Email](https://recoupable.dev/docs/api-reference/agents/verify) — POST /api/agents/verify - [List Templates](https://recoupable.dev/docs/api-reference/templates/list) — GET /api/agents/templates - [Create Template](https://recoupable.dev/docs/api-reference/templates/create) — POST /api/agents/templates - [Update Template](https://recoupable.dev/docs/api-reference/templates/update) — PATCH /api/agents/templates/{id} - ## Buy at 8x, sell to fans at 20x w/ Sean Peace Content ID: page:/podcast/sean-peace Source: https://recoupable.dev/podcast/sean-peace Published: 2026-10-18 Duration: 30 min Guest: Sean Peace, Founder, SongVest "Our target multiple is a 20X," says Sean Peace, who founded SongVest in 2007. A catalog owner buys at 8x, then sells 10% or 15% of a song to fans at 20x. Fans pay it because a SongShare is "the coolest piece of memorabilia you'll ever buy": a piece of a song they love that actually pays a royalty. We test that on one real offering. In SongVest's 2026 offering circular, a share of "Rain On Me" earned $12,637 over the last twelve months. The seller gets $252,750, about 20 times that, and fans pay up to $318,465, about 25 times. Sean explains who those buyers are, how payouts reach them, and why the first song he put on the marketplace, "Hit The Quan", was one he owned himself. We also cover fewer, bigger buyers after Sony and Recognition and BMG and Concord, what drives a multiple a year later, why a ton of money doesn't make you good at valuing catalogs, why song shares don't trade like stocks yet, and what AI licensing will actually pay song owners. ## Listen and watch - [spotify](https://open.spotify.com/episode/5kGHfRMSMh1rFgw4Vfep2a) - [youtube](https://www.youtube.com/watch?v=becKY4awx_o) - [SongVest](https://www.songvest.com/) - [LinkedIn](https://www.linkedin.com/in/seanpeace/) ## Transcript ### Sean Peace (0:00) Our target multiple is a 20X. They buy a catalog at an 8X, and they can sell 10% or 15% at a 20X. I think that's a pretty good value. The concept with SongShares is this is the coolest piece of memorabilia you'll ever buy, because you get to say you own a piece of a song, and you actually get to earn a royalty. ### Patrick Sweetman (0:26) Today, I'm chatting with Sean Peace, the president and founder of SongVest. SongVest has been trusted to provide access to shares and recordings by iconic artists including Beyoncé, Queen, TLC, Cardi B, Bad Bunny, Eminem, and Mariah Carey. Sean, welcome. ### Sean Peace (0:45) Thank you. Thanks for having me, Patrick. ### Patrick Sweetman (0:50) Let's kick it off by talking about who is still selling songs to fans. SongVest sells directly to fans. Unlike a lot of catalog owners that hold and maintain and don't give access to the people that actually listen to music, SongVest changes that and actually allows fans to own part of some of the most iconic songs that exist. JKBX left Reg A in twenty twenty-five. Musicow US filed to exit last month. SongVest is still listing new songs. So my question for you is, why are you still here? What did these other companies get wrong, and how have you guys been able to continue, continue with the vision? ### Sean Peace (1:27) Well, I guess the biggest thing is that I have been an entrepreneur basically my whole life. So I understand that it is... Th-there is no, there is no secret sauce, you know? There is no fast path. Um, you know, Instagram and things like that are, you catch lightning in a bottle. And when you are creating a brand-new market, just like I did when I created Royalty Exchange, you know, it's, it's hard, and it takes a long time. You know? I mean, we're trying to create a brand-new market, of which there is none, which is allowing fans to own a piece of a royalty stream. And I think one of the things that the other people got wrong... Well, I don't want to say got wrong. I mean, it's, it's just a different approach to the market, is they wanted to sell it as an investment, as an investment class. And I do that every day, right? I mean, it's, it's hard enough to sell music catalogs to high-net-worth individuals and get them to understand the value of a long-term, non-correlated alternative asset, right? I mean, that's a lot to try and grasp and understand. And our take, and my take immediately, was, you know, don't, don't sell this. Even though it is an investment, don't sell it as an investment, because who wants to buy a treasury bond, right? There isn't... You know, the world today is crypto. The world today is buy low, sell high. You know? The fun of it. Um, Polymarket. You know? I mean, take your pick. It's not, "Hey, I wanna go, you know, let me go get a 5% return on a bond." And so our take on the market is not, not treat it as an investment where someone, uh, you know, we're trying to tell someone to come in and spend $5 and get $10 back in, you know, 10 years or however long it's gonna be. It is the concept that, oh my God, I can own a piece of a song? Irrelevant of whether it pays you a dollar or a nickel or a penny, it's just that, you know, when is the last time that you bought, you know, a piece of vinyl, um, or a concert T-shirt, or if you're a super memorabilia fan, the, you know, stage-used guitar? When is the last time any of those paid you money back? Never. I mean, when you sell it, but if you've already bought it and it's a piece of memorabilia you love, like, why would you even wanna sell that? So I think that's our take that's a little bit different. You know? We, we are promoting this, uh, as a way to have kinda the, the coolest Pokémon card you could have, except that little piece of a song that you own actually pays you royalties. And, and it gets a little bit different if, you know, we're still trying to get artists to understand this concept and how they can engage with fans, but the game really changes once the artist is involved. Because now the artist has a direct connection with their fans. You know? Why are you selling this piece? What does it mean to you? What can it mean to the collaboration? You know, we, we call it elevating, uh, casual and super fans to invested fan. And what does it mean to be an invested fan? Well, if the artist is there, it can mean so many different things. But we're starting with, you know, songwriters and producers, um, where they aren't the actual artist that's singing the song. And so that's, that's our difference, right? And, and the other difference is, uh, you know, I knew this wasn't gonna be a flash in the pan, right? I knew that it was gonna take a long time. Um, and so, you know, I looked at other companies and the amount of capital they've raised, because there was another one, too. Exceed, uh, Exceed Cap or Exceed Talent. You know, they came along and did the same thing, right? I mean, so th-that's our difference. We know we're here for the long haul, and eventually it's gonna catch fire, and it, it really, uh, I mean, from our standpoint, it's doing exactly what we thought it was gonna do. Um, so we're gonna keep doing what we do and, you know, continue to grow the market. ### Patrick Sweetman (5:29) Personally, I love that there's an accessible option to retail. This actually leads into the fewer, bigger buyers. we know that Sony closed on the former Hipgnosis catalog in July of this year, twenty twenty-six. And this September, BMG and Concord merged. So there's now fewer bigger buyers. These buyers are now rolling up into bigger conglomerates, bigger syndicates. Where does a mid-sized catalog owner find a price nowadays with there being fewer larger buyers? ### Sean Peace (5:58) Yeah. So the, you know, we do, um Not only do we do song shares, but we do catalog sales. Uh, meaning helping producers, uh, songwriters, and even artists sell their catalog in that mid-tier. And that mid-tier could be anywhere from a small sale of $50,000 to a 10, $15 million catalog. Um, there's actually a lot more mid-tier buyers that keep coming into the market. Um, I would say some of them are sophisticated and some of them are not. Meaning, you know, it's It takes a long time to understand the value of a catalog, uh, and understand the value of what you're purchasing. Um, and then even to understand how you can maximize that value, you know? Primary Wave's the best company, without a doubt, in taking a catalog and extracting the most value they can out of that particular catalog. Um, so there's a I, I see more and more in the mid-tier. Where it's still harder is in that sub-2 million or sub-million, um, spot, because there you have to really rely on, um, high-net-worth individuals. So you have to go to a platform like SongVest or Royalty Exchange to get the mass amount of buyers to get the value. Um, but in that mid-tier, um, we've seen a lot more success in actually getting higher and higher multiples than you would've normally gotten just, like, two, you know, three years ago. So, um, so I think there's a, there's a growing market in that mid-tier size. ### Patrick Sweetman (7:28) So if I'm hearing you right, maybe the barbell ends are getting smaller. The biggest guys, there's fewer of them, and the smaller, there's not too much opportunity. But this mid is actually where the opportunity lies. Am I hearing you right? ### Sean Peace (7:38) Yeah. Yeah, I think so. I mean, it again, I, I think that folks are getting higher multiples in that million to, say, 10 million range than they were before. Uh, sub-million, um, you The problem is, um, legal costs, right? Due diligence and legal costs. So any, any true-up fund, um, it's really hard for them to transact under a million dollars, uh, just because they have a certain kind of set legal cost associated with every catalog. So it's hard for them to scale below a million. Um, but again, I think in that sweet spot, a million to 15 million, um, at least the way that we run, you know, um, our sales, meaning getting a, a catalog, analyzing it, and getting it out to those buyers. Um, the way that we do it, we've seen higher multiples than we have in the past. Yeah. And, and at the higher end, you know, I mean, those guys are still gonna fight over, you know, 25, 20 to 50 to $100 million catalogs. I mean, even though that space is getting more narrow, um, it's actually still growing, 'cause now Sony has a bigger pot of money that they can buy where they normally weren't buyers in the marketplace. You still have people that are coming into that particular arena. Um, so you're still gonna, you know, continue to have good multiples at the high end just because people want those bigger catalogs. ### Patrick Sweetman (9:04) Let's talk about those multiples. Last October, so one year ago, you wrote that publishing multiples had tightened to about 12 to 13x. Since then, Duetti released a survey that found more people saw multiples fall rather than rise. And this month, or I guess last month in September, the Fed raised rates. What number would you defend today in terms of multiples? ### Sean Peace (9:27) so it's hard to defend any kind of multiple because every catalog is different. So if you look at, for example, like Shot Tower, you know, they put a great report out every year. Um, I think they were in line as well saying that, uh, you know, larger catalogs are at that 12 to 13x range, uh, again, when you're buying publishing. But every catalog's different. I mean, Duet traditionally has played in masters. I think they're doing more now with, uh, some writers sharing publishing. Um, but they're buying You know, because they have so much money, they have to buy at scale. Uh, so they're buying catalogs at three, four, 5x, right? I mean, just because those songs are newer. Um, so date, you know, how old the catalog is, is the main driver of how much that catalog is worth. Um, and then after that, you're gonna look at specifics on the catalog. You know, are these larger songs that are, that are flat or slowly declining? You know, um, what, what does the longevity of these songs look like? Where's the income coming from? Is it a master? Is it a writer shares? Is it publishing? So, um, so it's really all over the place. And then again, we have the tiers. So is it in that small range, sub-million? 'Cause that's a different multiple from the 2 to 20 million, which is a different multiple from the 20 to the 100 million. Um, so there's a lot of factors that go into it ## Music licensing needs to be frictionless, not permissionless w/ Jesse Kirshbaum Content ID: page:/podcast/jesse-kirshbaum Source: https://recoupable.dev/podcast/jesse-kirshbaum Published: 2026-10-11 Duration: 28 min Guest: Jesse Kirshbaum, Founder & CEO, Nue Agency Jesse Kirshbaum signed the Clipse at 23. More than twenty years later, "Grindin'" is a Lavazza coffee. "Songs don't get old, but context is consistently changing," he says. That is his Living Catalog: brand partnerships that give older songs new meaning, and new income, long after the release date. Jesse runs Nue Agency and writes the Beats + Bytes newsletter. He explains how a brand picks a song (data shows where the heat is, taste shows where it is going), why music licensing needs to be frictionless rather than permissionless, and what a label sitting on 5,000 songs should pitch first: not a playlist, a concept. We cover Dolly Parton's Cup of Ambition coffee, DJ Khaled and Nesquik's "Another Bun", the 2025 NFL Draft walkout songs, Kid A Mnesia, and which archive deserves that treatment next. ## Listen and watch - [spotify](https://open.spotify.com/episode/0sVqvX9bANq4cpFAx5PFXL) - [youtube](https://www.youtube.com/watch?v=q8CsBbdmXqA) - [Nue Agency](https://nueagency.com/) - [Beats + Bytes](https://www.beatsxbytes.com/subscribe) - [LinkedIn](https://www.linkedin.com/in/jessekirshbaum/) ## Transcript ### Jesse Kirshbaum (0:00) songs like Drake's "Forever" 780,000 streams were gained during that period. Or Lil Wayne's Let It All Work Out, 646,000 streams just in that period. Songs don't get old, but context is consistently changing. That's how I think about the living catalog. ### Patrick Sweetman (0:27) Today I'm chatting with Jesse Kirshbaum, the founder and CEO of Nue Agency, an award-winning creative music and marketing agency positioned at the center of brands, technology, and entertainment. Welcome, Jesse. ### Jesse Kirshbaum (0:39) Thanks for having me, Patrick. ### Patrick Sweetman (0:41) Let's start by talking about "Grindin'" 20-plus years later. At 23, you signed The Clipse, and now "Grindin'" is a Lavazza coffee line. What happened to that song? ### Jesse Kirshbaum (0:53) It's amazing. Songs don't get old, but context is consistently changing. That's how I think about the living catalog. When I first found The Clipse, this was a gully street record. They were on this tour, and the song was just so novel and just so fresh with this guy named Pharrell out of Virginia, and he was, you know, making these hot beats. But this was kind of his first artist on his label, Star Trak, and it just felt like a new movement. And the fact that now Pharrell and Pusha are launching this coffee, it makes perfect sense to me, but if you probably told us 25 years ago that that was the case, they might not, none of us might not have seen that. But that's also because a whole changing dynamics between brands and artists and how this whole ecosystem has shifted. ### Patrick Sweetman (1:49) you wrote that being agentic is not just about advising, but it's about building. What has changed at New, and what are you building? ### Jesse Kirshbaum (1:59) My career has taken many different shapes. Uh, when I first started New 19 years ago, we were a talent agency, and that was what I knew, and that is how I kinda got my chops, was finding and breaking artists early. About 10 years ago, we changed our business model to representing brands, and in that world, I just saw that there was a shift where brands were trying to figure out how to work with music, and the music landscape was getting more complicated. So we just started to help build music strategy and advise brands on how best to connect with their consumers, leveraging the power of music. Again, the market's changing, and it's becoming a IP-rich world, and the biggest stories in music over the past decade aren't necessarily albums and tours. They're Dre and Beats. They're Rihanna and Fenty. They're Selena Gomez and Rare Beauty. I mean, these are artist collaborations or artist creations that are way bigger or way more valuable than the artists', these monster artists' entire catalogs combined. So it's just a really interesting time. And so for us as an agency- The concept is more what can we create with brands and artists, and more about how do we do something that's beyond just a pay-for-play endorsement? Let's create not just scale, but signal, and then take that signal and give it scale. So we're always looking for what deserves, what matters, what people will care about, and how to create something beyond just like, uh, Pharrell used to say, "I don't wanna be in a commercial where I just hold the cookie." And so really, like, we're trying to create partnerships where the artist is really vested and interested in the longevity of the product, the campaign, uh, the existence. So we're looking at IP. What can we create with our artist relationships and our brand relationships that are gonna have very meaningful value beyond just a, a, a television commercial? So like we just did, um, last year we did a, uh, a chocolate milk with Nesquik and DJ Khaled, and that was an original flavor that Khaled was... You know, I was living in Miami at the time, and we had just done a, a television commercial for, with Nesquik and for just their, their chocolate milk. It was the first time they'd ever done anything in music, and Khaled was explaining to me, you know, we were talking about what else we could do. Is there a re-up with them? And he was explaining, "I want the cinnamon to be the star." And he's like, "I want it to be cinnamon-forward." And he... I was like, "Holy cow, he's wants to produce a cinnamon-flavored, uh, chocolate milk. Like, this is, this is the guy to do it." And so we're talking to Nesquik, we're over, you know, with New York offices and Austria offices, and ultimately he was like, "I want it to be called Cinnabun." And they were texting me like, "We cannot clear Cinnabun, um, but we might be able to clear Cinnamon Bun." And, uh, you know, I said that to Khaled on the spot, and he was like, "Another bun." And, like, everybody's light went off, and it was like, we're gonna launch Another Bun, a cinnamon-flavored chocolate milk with Nesquik and DJ Khaled. And obviously the beauty of working with a brand like Nestle is you've got scale for distribution. So immediately we're in 14,000, um, stores. You know, we did a deal with 7-Eleven, we did a deal with Walmart, we did a deal with Target. We were in all the different Kroger's and all the kind of big box grocery stores, and Khaled gets a royalty on every one of these sold. Khaled's getting paid to be in the commercial as an endorsement, and it's the first time ever that Nestle's ever done anything in this capacity, and it just opens the door for more of these type of how can we do a royalty, an equity, a brand partnership type deal versus, like, me putting you in the commercial for the Super Bowl, which, you know, I don't think anyone's saying no to, but less and less interesting from a long-term perspective, from a cli- a consumer perspective, a fan perspective. They can kinda see through, uh, he was paid to be in that ad versus, oh, he actually cares about this, and he put some elbow grease into making this come to life, and he's here for a long haul, so let's give it a shot. ### Patrick Sweetman (6:57) So you know what brands want now. You have this line, not what can we pay you to promote, but what can we build together? What does that look like for a catalog owner? You ### Jesse Kirshbaum (7:06) Catalog is such an interesting space right now. People are amassing this IP. It's got value. You've got, like, very clear returns on streams, but I think there's just so much more inside of the songs and inside of the catalogs that make it even more powerful, scalable, robust, and valuable. So I, I think it's beyond just let's put this song in a commercial, because that's fine. That's a sponsorship. That's licensing. That's, um, status quo. It's more about how can we give this song new meaning through a brand partnership? And really the gold star, like the, the, the living catalog at its fullest reflection, is when you can find a part of a song, a verse, something inside of the song, and then that ends up giving the partnership new life and, and, and even new meaning. Like Dolly Parton, rest her soul, just released a Cup of Ambition coffee company. And to me, the nine-to-five woman that's got the ultimate hustle and positivity took... The team behind it took a lyric of her song and turned it into also a coffee brand, and that to me is, like, just a step beyond what licensing is looking like, right? If you just put "9 to 5" behind, you know, a Dunkin' Donuts commercial, it's cool, and I'm sure people would feel good about it, but if you could do that with a song lyric and turn that song lyric into a brand- And give it all new context. That's where these IP opportunities and, and are way more interesting. And I think brands are looking for new creative ways to connect, and these catalog holders, they have huge, huge investments and all of this inventory, and I think they're in an amazing opportunity to be innovative because they hold the heartstrings of so many of our memories in these songs. And these songs mean so much to us in different eras of our lives, and the familiarity makes it really convenient to be able to roll back out. ### Patrick Sweetman (9:37) The, the pushback that we hear is that some catalog investors say that brand deals don't really move the needle. Is, is that a fair argument, or how do you balance and pick a song for a campaign? ### Jesse Kirshbaum (9:49) I think that some of them cannot move the needle, and that's definitely true. I mean, in some regards, it's a payment, so that's good. But a really successful brand campaign is using both data and taste because data is gonna tell you where the heat exists, but taste is really gonna tell you where the heat is going or could be. The taste is the context. The taste is the cultural understanding. The taste is the connection points that might not be seen. Data's gonna tell you that streams and demographics and geography and social velocity and behavior is gonna-- what's gonna resonate. So it's a good component to how you should be finding songs, but really, it's about the sound and the flavor and what the artist has in mind and the story that you can tell. So it's-- there are times where this falls flat, but there are a lot of times also when done right that you can really change the trajectory of a brand's course. Like, culture marketing is not easy, but it's the wild card or it's the, you know, component to a marketing campaign or a brand's identity that could hockey stick growth. You know? All of a sudden, this certain song and this certain product and this certain endorsement, you just see it shooting up because it's in the right place at the right time. It's placed in the right movie. It's aligned with the right product or the right, uh, commercial, and everybody feels the charge of that halo effect. And, uh, I think that can happen with events. I think that can happen with partnerships. I think that can happen with celebrity part- endorsements. Um, culture marketing is not an easy guaranteed ROI the same way that, like, buying a Facebook ad would be or, you know, marketing on YouTube. You know you're getting your conversion rates there. But if you really want to be seen and, and, and experienced in a way that has a, a resonance, you gotta think about this in a way-- You gotta think about culture. And the way I look at music, music is the lowest barrier to entry into shifting culture. Music's the universal language. Music's the great connector. Some people might not be foodies. Some people might not like sports. But everybody connects to music in some capacity. And so music is that kind of great way to draw out emotion and, and to kinda ch ## AI music licenses will switch from market share to usage w/ Vickie Nauman Content ID: page:/podcast/vickie-nauman Source: https://recoupable.dev/podcast/vickie-nauman Published: 2026-10-04 Duration: 32 min Guest: Vickie Nauman, Founder & CEO, CrossBorderWorks Vickie Nauman expects the first AI music licenses, signed on market share, to start paying on actual usage when they come up for renewal in one to two years. The founder and CEO of CrossBorderWorks has spent 26 years licensing new music technology, and she lays out what rights holders need before that switch: an attribution system for AI output, artist opt-in, and a transparency clause in every AI license. We cover Spotify's AI artist badges, whether a Drake hit and a Kansas City singer-songwriter carry the same value in a training set, Merlin's role for independent catalogs, GEMA's court win against Suno, why AI-generated music should flow through the same DDEX pipes as everything else, and why 3.5 billion gamers make games a catalog income line. Vickie also explains why the industry lost four years trying to stop digital from 1999 to 2004, and why rights holders are at the table this time. ## Listen and watch - [spotify](https://open.spotify.com/episode/04QoyBh6NxS5occCDq77WG) - [youtube](https://www.youtube.com/watch?v=dhmwk6aK0HA) - [apple](https://podcasts.apple.com/us/podcast/ai-music-licenses-will-switch-from-market-share-to/id1316209308?i=1000793159857) - [CrossBorderWorks](https://www.crossborderworks.com/) - [LinkedIn](https://www.linkedin.com/in/vnauman) ## Transcript ### Vickie Nauman (0:00) it was four years of waste, complete wasted efforts because you cannot stop technology. You can help shape it. But in order to help shape it, you have to engage. ### Patrick Sweetman (0:19) Today I'm chatting with Vickie Nauman, the founder and CEO of CrossBorderWorks. They offer advisory and consulting services to help navigate the complex digital music landscape. Welcome, Vickie. ### Vickie Nauman (0:29) Thank you so much. ### Patrick Sweetman (0:31) Kicking it off, I wanna talk about something you were sharing on the last Music Tectonics podcast, where you were talking about human machine or a million shades of gray. In July, you were saying that in music, there's a million shades of gray in between human and machine. Spotify announced in August, and they just started rolling out in September, that if a s-- um, an AI persona badge. So they're gonna start labeling artist profiles with an AI badge to say this artist is AI. Now, you mentioned kind of it's not a polar AI or not AI, and so I'm curious from your perspective, if a songwriter uses AI for twenty percent of a track, which side of the badge would they fall on, and who's gonna get paid? ### Vickie Nauman (1:12) Yeah. It, it, it's, it's a, it's a really great debate, and I think with, with Spotify it's really interesting because they're focusing on the artist level, and that is very binary. There's either a real artist or there's a virtual artist. And so there's clarity there. Um, but I think where things become really gray are when you are a producer, you're a, a recording artist, you are a songwriter, engineer, mixing and mastering, all of these all of these tools are being used right now where it could be something as simple as a, you know, you needed a saxophone, and instead of calling a saxophone player to come to, you know, fly them into town, send them in the studio, do all the takes, you can go some-- you can go and generate that. And, um, and that may be a small part in a song, but this is for a you know, real human artist, but this is still an AI element. And are we going to go down the path of trying to label all of that when there are elements of AI being integrated? Because I see no end to AI-based samples, loops, uh, individual instrumentation, mixing, mastering, songwriting tools, all of that is going to be, you know, and is already being used by artists of all sizes. Um, and are we going to require labeling of all that? And does it matter? ### Patrick Sweetman (2:54) That's, that's where I land. Like, I scroll on LinkedIn, and I see people saying, "We have this new algorithm that says whether or not a track is AI, is made with AI." And from my own personal experience, like I've been doing this podcast for five or six years, and when I started, I had to manually go into, mm, Audacity and clip it and do everything by hand and upload it to Spotify by hand. And now I do a recording like this, and then I'll upload it into a software like Opus, and Opus is an AI clipper. It analyzes all the lyrics. It chops it. It puts up the open captions. It'll do clipping across. It'll throw in the B-roll. So does that mean that that should get labeled as AI? Like it's, it's the editor that's AI. The video is us. We're humans. We're skin and bone. But Great. the editor's an AI, and now I'm getting into using Opus for more motion graphics. And I was using ElevenLabs to do an AI voice, and then I had someone tell me, "Ah, that's too uncanny valley." And so now I'm recording my own voice over AI motion graphics. And so like on my side as an artist, I'm balancing between, "Ah, people think that's too uncanny valley. Let me do that myself," or, "People are okay with this. They actually like it. It amplifies the quality of what I would used to have to spend five or six hours on." And so when people come out with these badges, and they try to make it a binary conversation, it makes me really wonder, like is-- what, what's the goal? I mean, from, from your perspective, do you, do you see these AI technologies the same as like an Auto-Tune or a digital DAW or like a technology that starts off, and it freaks out all the artists, and then over time, we realize this is a part of the process? Or do you see AI technology being fundamentally different ### Vickie Nauman (4:31) I think where it is right now, it is way more disruptive than other kinds of technologies because we have litigation happening, and we have real lack of clarity around training data and fair use. Um, you know, if we were in another period of time, I think our government would probably be all over this trying to create constraints around AI. But we are in 2026, um, and every country is tackling this and really looking at it differently. So we have ambiguity, and we do have, um, very disruptive technology out there where people can create any music they want and upload it and get it into streaming services, and then that competes with human-made music. But I believe that part of it, the litigation, the constraints, the inevitable migration to a palatable business model that the music industry, labels, publishers, artists are going to support, that's going to change. And how it changes, I don't know. Um, but I think, I think where the real value is, is going-- the real sustainable long-term value will be in tool creation. And I think of this not that differently than, as you say, Auto-Tune, um, drum machine, um, samples and loops. You know, it was not that long ago when a lot of musicians and more traditional thinkers did not consider DJs to be actual musicians because they're using other people's works, and they were mixing and remixing, and they're doing turntablism. I believe they are artists, and I've always believed that they're artists. And so I think that I always follow these trends and really, you know, how are artists adapting these? And they're-- Artists are usually pretty friendly, but what they don't want is people out creating songs that imitate them and that could potentially be using their own music to create a competitive product to the artists themselves. And I think that that is the piece of it that is, is pretty messy right now, but I think we'll get beyond it. ### Patrick Sweetman (6:49) The piracy piece is something you were talking about in the last recording as well. You, you were mentioning before in the last generation there was-- people would take ripped music and then they'd upload it onto these peer-to-peer sites and-- but you always knew this is a song by Drake, this is a song by The Rolling Stones. But you were mentioning in this generation, it's much more like a blender. Like, you chuck all these songs in into a training model, and then you train it, and after that you, you go over to Suno and you generate a song and was this referencing The Rolling Stones' work? We don't really know. And so we've got like 25 years of new tech meeting old rights. And Yeah. so once again, the majors are gonna get the lion's share while independents wait. And you watched this happen with downloads, streaming, and games. Is AI repeating the same pattern? And did anyone ever catch up who kind of waited for everything to play out? ### Vickie Nauman (7:45) Yeah. I know. It's, it's, it's really fascinating, and it is, it is kind of surprising when I hear the words come out of my mouth that I've been doing this for 26 years, um, because that seems like that seems like so long. But in my perspective, it feels like it has not been 26 years. Um, but, you know, in those early days, we were able to follow where there was piracy. We were able to find music, find recordings that were being used in an unlicensed way, and we don't really have the ability to do that now. Um, there's a lot of companies that are starting, you know, to have different levels of detection. I think the industry wants a version of a fingerprinting service that can go out and identify truly generated songs that have music of theirs in them as an infringing song. It's very hard to do. Um, but I also think that if, if I, if I fast-forward and I think about what, what do we need in the landscape for this to really work? And for me, those things are there's some end user, mass consumer, mainstream use that fans and people who love music are going to want to do in a sustainable way. And it has to be acceptable to labels, publishers, artists, songwriters, and there has to be some sort of value exchange. And so we need all three of those legs in order to make AI and generative music work in the marketplace, and we aren't there yet. We, we, we don't yet have, um, have clear and visible business models, um, and we also don't have-- I don't think that we we, we have that use case for mainstream consumers. Um, I know there are a lot of people that are creating music that are just noodling around, and they're not maybe-- maybe they're not musicians at all, but they're just playing around with it. They want to release that into DSPs. There are constraints happening on that right now, um, to try to, to try to, you know, further differentiate and have some sort of filter so that there's not just an open pipe into DSPs of hundreds of millions of AI-generated songs. But I think that-- I think we have to have the market develop, and I believe that one of the pieces of this, of this future state of a market that is going to be required to get everyone paid is an attribution system for all output that has been created. And I advise one company called Musical AI, and they've built a consent management and an attribution system, and we're, you know, we're deep in technical development, deep in partnership development. We're showing this off to a lot of rights holders, and I think that we do have a... You know, you talk about patterns. I'm always about pattern recognition, and there is a very typical way that when new technology comes onto the scene, how it's licensed, and it's usually the initial deals are maybe two, one to two year long deals with major rights holders. And those platforms, they are almost always done on market share because you don't have data about how, you know, how people are using your service or what music is being surfaced. But at the end of the two-year period, there is-- it shifts from market share to more known data on each of these platforms, and that is when I think that we have to have everything in place for the pipes and ## The $100K playbook of an independent artist w/ Xcelencia Content ID: page:/podcast/xcelencia Source: https://recoupable.dev/podcast/xcelencia Published: 2024-12-08 Duration: 40 min Guest: Xcelencia, Independent artist and technologist Recorded in Miami during Art Basel 2024. Xcelencia runs his career the way a good company runs a product line: the album treated as software, distribution engineered through quests rather than playlist luck, and a direct commercial relationship with the collectors who fund the work. We go through the numbers behind his $100K year, the tooling he built versus bought, token-bound accounts as fan infrastructure, and what he would keep if he started again. Required listening for anyone who owns a catalog and wants to see the leading edge of independent operation in practice. ## Listen and watch - [spotify](https://open.spotify.com/episode/4gIQjPCwCOpNdzoi8HQDjL) - [youtube](https://www.youtube.com/watch?v=ZXLjcYggrwc) - [apple](https://podcasts.apple.com/us/podcast/the-%24100k-playbook-of-an-independent-artist-w-xcelencia/id1316209308?i=1000679696884) - [X](https://x.com/xcelencia) - [Spotify](https://open.spotify.com/artist/7c34Wg8HcWO9BWhHLwiXsr) ## Growing a music platform: what worked at Mint Songs w/ Nick Merich Content ID: page:/podcast/nick-merich Source: https://recoupable.dev/podcast/nick-merich Published: 2022-07-04 Duration: 63 min Guest: Nick Merich, Head of Growth, Mint Songs Nick Merich ran growth at Mint Songs while it went from a first release to a second protocol version on Ethereum. Fresh from NFT NYC, he lays out the parts of growth that were repeatable: how the team chose which artists to launch, what a release week looked like operationally, which metrics they reported to investors and which they ignored, and what a platform owes the artists who trust it with their first sale. A candid account of building distribution from zero, from the person who had to make the numbers move. ## Listen and watch - [spotify](https://open.spotify.com/episode/2BGKNlumOWCEdpcZMMnh2O) - [youtube](https://www.youtube.com/watch?v=0TFRtda1xHg) - [apple](https://podcasts.apple.com/us/podcast/growing-a-music-platform-what-worked-at-mint-songs/id1316209308?i=1000568762506) - [X](https://twitter.com/Nick_Merich) ## From Grammy-winning producer to building software for the music business w/ Sidney Swift Content ID: page:/podcast/sidney-swift Source: https://recoupable.dev/podcast/sidney-swift Published: 2022-07-18 Duration: 60 min Guest: Sidney Swift, Cofounder, Recoup Sidney Swift has produced records that won Grammys, holds a US patent, and now runs Recoup, where labels and rights companies come when they need an AI strategy that works. This conversation was recorded months before we started working together, and it is the origin of the way we operate: a long, specific conversation first, the contract later. We cover what a producer learns about leverage, why most music-tech pitches fail the people who actually run catalogs, and what it takes to build something a finance team will trust with royalty data. ## Listen and watch - [spotify](https://open.spotify.com/episode/4KCglIfrD8i5JdN2c1gZlF) - [youtube](https://www.youtube.com/watch?v=z_3qo3YVPWQ) - [apple](https://podcasts.apple.com/us/podcast/from-grammy-winning-producer-to-building-software-for/id1316209308?i=1000570288536) ## Company: About Recoup Content ID: page:/company Source: https://recoupable.dev/company Representation: Summary of the public page. Follow the source for the complete page. Recoup builds AI systems for the business of music and helps the people behind it put those systems to work. Explore the company, its vision, and Recoup Records, the in-house label that uses the platform. ## Next steps - [About Recoup](https://recoupable.dev/about) - [Our vision](https://recoupable.dev/company/vision) - [Recoup Records](https://recoupable.dev/records) ## Recoup’s vision for AI in music Content ID: page:/company/vision Source: https://recoupable.dev/company/vision Representation: Summary of the public page. Follow the source for the complete page. Research, coordination, and repetitive preparation take time from music teams. Recoup builds toward useful results: prepared reports, supported research briefs, release plans, and recurring workflows. People set the direction, define good work, and review decisions. Platform, Skills, API, advisory, custom systems, and training offer different ways to put those methods to work. ## Next steps - [Platform](https://recoupable.dev/platform) - [Skills](https://recoupable.dev/skills) - [Services](https://recoupable.dev/services) ## Recoup Records: Our in-house music label Content ID: page:/records Source: https://recoupable.dev/records Representation: Summary of the public page. Follow the source for the complete page. The in-house label puts Recoup’s own workflows into practice: understand the artist, audience, voice, and goals; develop campaigns and release plans; use the results to improve the methods and tools. ## Next steps - [Platform](https://recoupable.dev/platform) - [Skills](https://recoupable.dev/skills) ## AI for music funds, catalog owners, labels, distributors, and artists Content ID: page:/solutions Source: https://recoupable.dev/solutions Representation: Summary of the public page. Follow the source for the complete page. Funds and catalog owners can organize acquisition documents, compare royalty statements and receipts, search permissioned catalogs, and prepare management briefs. Labels and rightsholders can build repeatable artist research, creative preparation, reporting, and release planning. Distributors can connect roster onboarding, cross-roster reporting, embedded tools, and artist services. Artists and managers can bring artist context into audience research, campaigns, release plans, and scheduled tasks. ## Next steps - [Acquisition review](https://recoupable.dev/acquisitions) - [Platform](https://recoupable.dev/platform) - [Custom systems](https://recoupable.dev/build) - [Pricing](https://recoupable.dev/pricing) ## Recoup Lab: Exploring AI in the business of music Content ID: page:/lab Source: https://recoupable.dev/lab Representation: Summary of the public page. Follow the source for the complete page. Each research direction starts with a real job and a specific test: coordinate a release with clear deliverables and human input; reconcile a known royalty statement without inventing matches; compare permissioned catalog selections against expert review of a creative brief. These are research directions, not claims that the experiments have met their targets. ## Next steps - [Open source](https://github.com/recoupable/skills) - [Skills](https://recoupable.dev/skills) - [Contact](https://recoupable.dev/contact) ## Recoup resources: Guides, demos, tools, and documentation Content ID: page:/resources Source: https://recoupable.dev/resources Representation: Summary of the public page. Follow the source for the complete page. Try interactive royalty and diligence examples, develop a workflow plan, check readiness, model time and cost assumptions, or explore catalog valuation. Read the playbook and blog, install Skills, and use the API documentation to build. ## Next steps - [Demos](https://recoupable.dev/learn/demos) - [Workflow plan](https://recoupable.dev/workflow-plan) - [Readiness](https://recoupable.dev/audit) - [ROI planner](https://recoupable.dev/roi) - [Valuation](https://recoupable.dev/valuation) - [Playbook](https://recoupable.dev/playbook) - [Blog](https://recoupable.dev/blog) - [Docs](https://recoupable.dev/docs) - [Skills](https://recoupable.dev/skills) - [Compare](https://recoupable.dev/compare) ## Recoup demos: See AI in the work Content ID: page:/learn/demos Source: https://recoupable.dev/learn/demos Representation: Summary of the public page. Follow the source for the complete page. The royalty walkthrough compares statements with receipts and keeps unresolved differences visible by payment source. The acquisition walkthrough connects missing information, conflicting identifiers, and seller questions to supporting records. Open the platform to work with your own artist context. ## Next steps - [Royalty reporting example](https://recoupable.dev/operations#royalty-example) - [Acquisition example](https://recoupable.dev/acquisitions#acquisition-example) - [Platform](https://recoupable.dev/platform) ## Catalog valuation with Recoup Content ID: page:/valuation Source: https://recoupable.dev/valuation Representation: Summary of the public page. Follow the source for the complete page. Search for an artist and sign in to run the valuation with your Recoup account. Running the tool uses agent credits. The directional estimate does not account for every right, contract term, income source, or expense in a purchase. Reading this document does not run a valuation. ## Next steps - [Run the valuation](https://recoupable.dev/valuation) - [Discuss acquisition review](https://recoupable.dev/acquisitions/contact) ## Choose how to work with Recoup Content ID: page:/compare Source: https://recoupable.dev/compare Representation: Summary of the public page. Follow the source for the complete page. Use the platform for artist context, conversations, scheduled tasks, and reporting. Bring open-source Skills into compatible agents. Scope custom systems around company data, tools, and workflows. Use the authenticated API to build capabilities into your product. With self-service tools, your team owns context, integrations, methods, training, and maintenance. A Recoup engagement can include structuring information, building connections, testing a defined workflow, training, and documented handoff. ## Next steps - [Platform](https://recoupable.dev/platform) - [Skills](https://recoupable.dev/skills) - [Build](https://recoupable.dev/build) - [API documentation](https://recoupable.dev/docs) - [Advisory](https://recoupable.dev/advisory) ## The AI Music Marketing Playbook Content ID: page:/playbook Source: https://recoupable.dev/playbook Representation: Summary of the public page. Follow the source for the complete page. Read seven chapters, choose a useful task, and adapt the workflow to your artist or team. Give the task context, review the output, and repeat what helps. The complete guide is available to read, print, or download as Markdown. ## Next steps - [Read the playbook](https://recoupable.dev/playbook/download) - [Download Markdown](https://recoupable.dev/playbook/recoup-ai-music-playbook.md) - [Custom systems](https://recoupable.dev/build) ## Discuss your catalog workflow Content ID: page:/operations/contact Source: https://recoupable.dev/operations/contact Representation: Summary of the public page. Follow the source for the complete page. Use the inquiry form to describe your workflow and contact details, or email Recoup. The visitor reviews and submits the inquiry; reading this page does not send a message or book a meeting. ## Next steps - [Inquiry form](https://recoupable.dev/operations/contact) - [Explore the workflow](https://recoupable.dev/operations) - [Email Recoup](mailto:hi@recoupable.dev) ## Discuss your acquisition workflow Content ID: page:/acquisitions/contact Source: https://recoupable.dev/acquisitions/contact Representation: Summary of the public page. Follow the source for the complete page. Use the inquiry form to describe your workflow and contact details, or email Recoup. The visitor reviews and submits the inquiry; reading this page does not send a message or book a meeting. ## Next steps - [Inquiry form](https://recoupable.dev/acquisitions/contact) - [Explore the workflow](https://recoupable.dev/acquisitions) - [Email Recoup](mailto:hi@recoupable.dev) ## Recoup plugin for your music business Content ID: page:/label-in-a-box Source: https://recoupable.dev/label-in-a-box Representation: Summary of the public page. Follow the source for the complete page. The plugin provides repeatable methods for artist research, releases, content, and catalog work, plus the Recoup MCP connection. Starter is $19 USD per month until canceled and includes $20 in monthly Recoup usage credits. Your AI subscription and third-party services are separate. Install using the official Codex, Claude Code, or Cursor instructions and sign in to Recoup. ChatGPT distribution remains subject to submission and review. Public Skills are also available separately. Check the source page for checkout availability. ## Next steps - [Plugin offer](https://recoupable.dev/label-in-a-box) - [Get your download](https://recoupable.dev/label-in-a-box/setup) - [Public skills](https://recoupable.dev/skills) ## Set up your Recoup plugin Content ID: page:/label-in-a-box/setup Source: https://recoupable.dev/label-in-a-box/setup Representation: Summary of the public page. Follow the source for the complete page. Open your private purchase link or sign in to verify access and download the plugin. Follow the included installation guide and connect your Recoup account. This public overview contains no purchase links, account state, or private downloads. ## Next steps - [Plugin setup](https://recoupable.dev/label-in-a-box/setup) - [Plugin details](https://recoupable.dev/label-in-a-box) ## Recoup Brand Studio Content ID: page:/brand Source: https://recoupable.dev/brand Representation: Summary of the public page. Follow the source for the complete page. The Brand Studio is a creative library with its own navigation. Finals, experiments, and carousel tools support reviewing brand assets. Browser review selections and unpublished source files are not part of this public document. ## Next steps - [Finals](https://recoupable.dev/brand) - [Experiments](https://recoupable.dev/brand/experiments) - [Carousels](https://recoupable.dev/brand/carousels) ## Recoup brand experiments Content ID: page:/brand/experiments Source: https://recoupable.dev/brand/experiments Representation: Summary of the public page. Follow the source for the complete page. The Brand Studio is a creative library with its own navigation. Finals, experiments, and carousel tools support reviewing brand assets. Browser review selections and unpublished source files are not part of this public document. ## Next steps - [Finals](https://recoupable.dev/brand) - [Experiments](https://recoupable.dev/brand/experiments) - [Carousels](https://recoupable.dev/brand/carousels) ## Recoup brand carousels Content ID: page:/brand/carousels Source: https://recoupable.dev/brand/carousels Representation: Summary of the public page. Follow the source for the complete page. The Brand Studio is a creative library with its own navigation. Finals, experiments, and carousel tools support reviewing brand assets. Browser review selections and unpublished source files are not part of this public document. ## Next steps - [Finals](https://recoupable.dev/brand) - [Experiments](https://recoupable.dev/brand/experiments) - [Carousels](https://recoupable.dev/brand/carousels) ## AI training for music teams Content ID: page:/training Source: https://recoupable.dev/training Representation: Summary of the public page. Follow the source for the complete page. Learn on the work you already do: artist research, release planning, catalog operations, and reporting. We help your team use AI, check its output, and turn useful results into repeatable workflows. Start with a real task. Bring a release brief, a catalog question, or a recurring report. Learn where AI helps and where your judgment matters. Learn to check the work. Give the agent useful context, trace answers to their sources, and spot what needs a human review. Leave with a method to reuse. Capture the instructions, reference material, and review steps so your team can repeat the workflow. We shape the training around your team’s experience and the software you use. Scope, format, and price are agreed before the engagement. Do we need technical experience? No. We adapt the training to the people doing the work, from first-time AI users to teams already working with agents and automations. Do we need to use Recoup Platform? No. Training can use Recoup Platform or the AI tools your team already uses. We agree on the tools and access needed before the session. Can you train us on a system you built? Yes. Training can form part of a custom build and handoff, or focus on improving how your team uses an existing system. What does training cost? It depends on the team, workflows, and depth of support. We agree on the scope, format, and price with you before starting. ## Next steps - [Discuss team training](https://recoupable.dev/start-project?workflow=Team%20training) - [Advisory](https://recoupable.dev/advisory) - [Build](https://recoupable.dev/build) ## AI strategy, custom systems, and team training Content ID: page:/services Source: https://recoupable.dev/services Representation: Summary of the public page. Follow the source for the complete page. AI strategy: review workflows, data, and tools; agree on priorities, a practical roadmap, project scope, and success measures. Custom systems: build agents, applications, and connections to existing tools, with documented code and a working handoff. Examples include royalty reporting and investment review. Team training: practice on real workflows, capture reusable methods and review practices, and prepare internal owners to keep improving the system. Scope, deliverables, and price are agreed before work begins. Custom code is delivered in a repository the client controls; the agreement defines ownership and third-party component terms. Ongoing support and maintenance can be scoped separately. ## Next steps - [AI advisory](https://recoupable.dev/advisory) - [Custom builds](https://recoupable.dev/build) - [Team training](https://recoupable.dev/training) - [Compare plans](https://recoupable.dev/pricing#plans) - [Discuss a project](https://recoupable.dev/contact) ## AI advisory for music companies Content ID: page:/advisory Source: https://recoupable.dev/advisory Representation: Summary of the public page. Follow the source for the complete page. Recoup reviews recurring work in reporting, research, release planning, and catalog operations. The assessment considers the task, its current cost, the available information, access, and the places where judgment matters. The engagement identifies priorities, dependencies, a focused first project, and success measures. It can cover one question, a department, or a company-wide plan. Advisory can use Recoup, other tools, or custom software. It does not require moving the team onto the Recoup platform. The people doing the work participate in shaping the plan. ## Next steps - [Advisory plan](https://recoupable.dev/pricing#advisory) - [Discuss AI strategy](https://recoupable.dev/start-project?plan=advisory&billing=monthly) - [Custom systems](https://recoupable.dev/build) ## Custom AI systems for music businesses Content ID: page:/build Source: https://recoupable.dev/build Representation: Summary of the public page. Follow the source for the complete page. Recoup builds agents for research, reports, campaign briefs, and recurring tasks; integrations connecting catalog data, royalty statements, CRMs, and internal tools; dashboards with source records and review queues; and full applications with interfaces and backends. Delivery starts with an agreed task, inputs, people, and acceptance standard. Useful pieces are reviewed with the team, tested on real work, documented, and handed over with training. Custom code is delivered in a repository the client controls. Agreements define ownership, component terms, price, maintenance, and responsibilities. Builds may use Recoup or run as standalone software. ## Next steps - [Build + Partner plan](https://recoupable.dev/pricing#partner) - [Discuss a custom system](https://recoupable.dev/start-project?plan=partner&billing=monthly) - [Royalty reporting](https://recoupable.dev/operations) - [Acquisition review](https://recoupable.dev/acquisitions) ## The agentic music company atlas. Content ID: page:/atlas Source: https://recoupable.dev/atlas Representation: Summary of the public page. Follow the source for the complete page. From catalog to cash flow. How people, rights, money and AI agents connect. A connected operating model: the people, information, decisions and work that make a music company run. ## Next steps - [Explore the Map](https://recoupable.dev/atlas) - [Custom builds](https://recoupable.dev/build) - [Start a project](https://recoupable.dev/start-project) ## Recoup Platform: a ready-to-use AI workspace for music teams Content ID: page:/platform Source: https://recoupable.dev/platform Representation: Summary of the public page. Follow the source for the complete page. A chat-based agent workspace built for music business operations. Recoup hosts the app and connects the tools, so your team can start working without building its own setup. Work across your artists, catalogs, and releases through chat. For music teams that want AI without a custom build. No development team required. Start with a hosted workspace. Skip the cost of building and maintaining your own AI system. AI built for music operations. Go beyond general chat with tools and context for your artists, catalogs, releases, and campaigns. Your agents. Your branded portal. Give artists and clients a white-labeled portal to work with your agents under your brand. Research: Get artist and audience insights to shape your next campaign. Create content: Turn tracks and release plans into campaign ideas, visuals, and copy. Automate repeat work: Schedule research, content prompts, and reports for your roster or catalog. Hosted by Recoup. Open source. Use the cloud app while Recoup runs the infrastructure. The workspace is built on the Recoup API and MCP, and its code is open source. Already have an AI workspace? Connect Recoup’s tools. Keep using the agent you prefer. Add music workflows with Recoup Skills, or connect it to Recoup’s tools through the API and MCP. Need a custom system? We can build it with you. Work with our team on bespoke agents, integrations, and workflows built around your company. An artist, catalog, or release you’re working on, plus any useful files and notes. Give Recoup a clear task, such as researching an audience or drafting a release campaign. The self-serve Recoup Platform plan includes workspace access and the music skill pack at $99/month. Annual billing saves at least 20%. API and MCP usage is billed separately by usage. Company-specific workflows and integrations can be scoped with the consulting team. ## Next steps - [Open Recoup Platform](https://app.recoupable.dev/) - [Recoup Platform plan](https://recoupable.dev/pricing#platform) - [Explore the app source](https://github.com/recoupable/app) - [Explore Skills](https://recoupable.dev/skills) - [Connect the API or MCP](https://recoupable.dev/developers) - [Custom systems](https://recoupable.dev/build) ## Recoup pricing: Platform, advisory, and custom builds Content ID: page:/pricing Source: https://recoupable.dev/pricing Representation: Summary of the public page. Follow the source for the complete page. Platform: $99/month, or $79/month billed annually at $948. The tools and music skills. You take it from here. Includes: Recoup Platform access; Music skill pack; Artist context and research; Content and recurring workflows. Advisory: $999/month, or $799/month billed annually at $9,588. An expert in your corner. A clear plan for your team. Includes: AI strategy and workflow review; A prioritized action plan; Advice on tools and implementation; Team coaching and adoption. Build + Partner: $9,999/month, or $7,999/month billed annually at $95,988. Your AI build partner. From the first idea to daily use. Includes: Custom agents and applications; Connections to your tools and data; Testing, documentation, and handoff; Team training and ongoing improvements. Annual billing applies a 20% discount, then rounds the monthly equivalent down to a whole dollar. The annual charge is twelve times that rounded amount. The page shows both amounts. Enterprise has a custom scope and price; contact Recoup to discuss the engagement. For advisory and builds, scope, deliverables, and delivery schedule are agreed together. API and MCP are billed separately by usage. Consult the credits documentation for billable operations and usage accounting. The annual subscription discount does not discount usage charges. Recoup Skills remains open source. The Recoup Platform plan bundles a hosted workspace and the music skill pack; an AI client or third-party services may have their own costs. ## Next steps - [Compare plans](https://recoupable.dev/pricing#plans) - [Open Recoup Platform](https://app.recoupable.dev/) - [Discuss advisory](https://recoupable.dev/start-project?plan=advisory&billing=monthly) - [Discuss a build](https://recoupable.dev/start-project?plan=partner&billing=monthly) - [Discuss Enterprise](https://recoupable.dev/start-project?plan=enterprise) - [API and MCP usage](https://recoupable.dev/docs/credits) ## Recoup Skills: music playbooks for your AI Content ID: page:/skills Source: https://recoupable.dev/skills Representation: Summary of the public page. Follow the source for the complete page. Recoup Skills brings music-business instructions, tools, and templates into compatible AI agents. The collection covers artist research, release planning, content, catalog work, and opportunity research. A skill is a reusable method for a job. It complements the tools an agent can access; some skills call the Recoup API or other services. The hosted Recoup app provides a workspace. Skills bring methods into an agent you already use. Installation and available integrations depend on the chosen AI client. ## Next steps - [Recoup Skills repository](https://github.com/recoupable/skills) - [Install in Claude](https://recoupable.dev/docs/claude) - [API authentication](https://recoupable.dev/docs/authentication) ## Recoup REST API, MCP, CLI, and agent tools Content ID: page:/developers Source: https://recoupable.dev/developers Representation: Summary of the public page. Follow the source for the complete page. Recoup exposes a REST API, an authenticated platform MCP server, a CLI, and open-source Skills. The public documentation describes request parameters, responses, and credits. API and MCP are billed by usage, separately from subscriptions. The credits guide explains which operations incur charges and how usage is measured. The documented platform MCP endpoint is https://api.recoupable.dev/mcp. Its setup flow uses OAuth and scope-dependent tool discovery; verify sign-in, workspace access and available tools in your chosen host. REST scripts need separately configured credentials; public website reading and search are separate tools. The CLI is installed as @recoupable/cli and uses RECOUP_API_KEY. Available commands and current beta limitations are documented. Keep API keys on the server and consult the individual endpoint's authentication requirements. ## Next steps - [API reference](https://recoupable.dev/docs/api-reference) - [Platform MCP setup](https://recoupable.dev/docs/mcp) - [API and MCP pricing](https://recoupable.dev/pricing#usage) - [Usage credits](https://recoupable.dev/docs/credits) - [CLI guide](https://recoupable.dev/docs/cli) - [API key setup](https://recoupable.dev/docs/authentication) ## About Recoup Content ID: page:/about Source: https://recoupable.dev/about Representation: Summary of the public page. Follow the source for the complete page. Recoup was founded by Sidney Swift. It builds tools for artists and teams and custom AI systems for the work inside music companies. The approach is to understand the task before choosing technology, build with the people doing the work, document the system, and help the team take responsibility for it. ## Next steps - [Services](https://recoupable.dev/services) - [Platform](https://recoupable.dev/platform) - [Contact](https://recoupable.dev/contact) ## Talk to Recoup about your company Content ID: page:/contact Source: https://recoupable.dev/contact Representation: Summary of the public page. Follow the source for the complete page. Bring your company's priorities, an existing process, or a part of the business you want to improve. Recoup can discuss where AI may help and a useful starting point. The contact form asks for a name, work email, company, area of interest, and project brief. Use the form or email the address below. Reading this website does not submit an inquiry or book a meeting. ## Next steps - [Project inquiry form](https://recoupable.dev/contact) - [Start a project](https://recoupable.dev/start-project) - [Email Recoup](mailto:hi@recoupable.dev) ## Start your project with Recoup Content ID: page:/start-project Source: https://recoupable.dev/start-project Representation: Summary of the public page. Follow the source for the complete page. Recoup emails you to arrange a consultation. Custom AI systems connect company knowledge and tools to help teams find answers and turn research, reporting and recurring tasks into AI workflows. The conversation establishes the system’s scope and next steps. Submitting the form does not book a meeting or produce a quote. Scope and pricing are agreed before work begins. An allowlisted plan selection retains its billing context. The project inquiry requires a name, email, area of interest, initial project budget in USD, and preferred starting timeframe. Current tools are optional. An expandable section offers an optional company name and note. The email domain is saved as an unverified company research hint. Budget ranges are planning context, not a rate card. Not decided yet and Just exploring are valid starting points. Recoup agrees scope and price before work begins. The visitor reviews and submits the brief. When direct submission is unavailable, the page prepares an unsent email brief with selectable and copyable text. Reading the page or preparing a brief through an agent does not save a lead or contact Recoup. ## Next steps - [Start a project](https://recoupable.dev/start-project) - [Contact form](https://recoupable.dev/contact) - [Custom builds](https://recoupable.dev/build) ## Recoup Podcast: how music businesses use AI Content ID: page:/podcast Source: https://recoupable.dev/podcast Representation: Summary of the public page. Follow the source for the complete page. The people running music businesses, on their history in the industry and how they actually use AI today. Thirty to sixty minutes with founders, fund managers, publishers and catalog owners. No pitches. Practical answers you can take back to your team. Buy at 8x, sell to fans at 20x w/ Sean Peace, Founder, SongVest (2026-10-18, 30 min). YouTube: https://www.youtube.com/watch?v=becKY4awx_o. Spotify: https://open.spotify.com/episode/5kGHfRMSMh1rFgw4Vfep2a. Music licensing needs to be frictionless, not permissionless w/ Jesse Kirshbaum, Founder & CEO, Nue Agency (2026-10-11, 28 min). YouTube: https://www.youtube.com/watch?v=q8CsBbdmXqA. Spotify: https://open.spotify.com/episode/0sVqvX9bANq4cpFAx5PFXL. AI music licenses will switch from market share to usage w/ Vickie Nauman, Founder & CEO, CrossBorderWorks (2026-10-04, 32 min). YouTube: https://www.youtube.com/watch?v=dhmwk6aK0HA. Spotify: https://open.spotify.com/episode/04QoyBh6NxS5occCDq77WG. The $100K playbook of an independent artist w/ Xcelencia, Independent artist and technologist (2024-12-08, 40 min). YouTube: https://www.youtube.com/watch?v=ZXLjcYggrwc. Spotify: https://open.spotify.com/episode/4gIQjPCwCOpNdzoi8HQDjL. Growing a music platform: what worked at Mint Songs w/ Nick Merich, Head of Growth, Mint Songs (2022-07-04, 63 min). YouTube: https://www.youtube.com/watch?v=0TFRtda1xHg. Spotify: https://open.spotify.com/episode/2BGKNlumOWCEdpcZMMnh2O. From Grammy-winning producer to building software for the music business w/ Sidney Swift, Cofounder, Recoup (2022-07-18, 60 min). YouTube: https://www.youtube.com/watch?v=z_3qo3YVPWQ. Spotify: https://open.spotify.com/episode/4KCglIfrD8i5JdN2c1gZlF. Run a label, a fund, a publisher or a management company? Come tell your story. ## Next steps - [Watch on YouTube](https://www.youtube.com/playlist?list=PLGjXo1TfTj0w) - [Listen on Spotify](https://open.spotify.com/show/2qOKySbQkldohO7vbSGIYz) - [Listen on Apple Podcasts](https://podcasts.apple.com/us/podcast/recoup-podcast/id1316209308) - [Request an invite](https://recoupable.dev/start-project?workflow=Podcast%20guest) ## What could AI do for your music business? Content ID: page:/workflow-plan Source: https://recoupable.dev/workflow-plan Representation: Summary of the public page. Follow the source for the complete page. Chat with Recoup's AI assistant about the work slowing your team down. It creates a proposed workflow plan with a concrete deliverable, required inputs, a small first test, human review and a success measure. Refine the plan in conversation and download it without providing contact details. The assistant can also answer questions using public Recoup sources. Messages are processed by AI and stored to support the conversation. Do not share confidential records. It does not access private catalogs, send email, or implement the proposed workflow. ## Next steps - [Start my plan](https://recoupable.dev/workflow-plan) - [Custom systems](https://recoupable.dev/build) ## Ask Recoup Content ID: page:/ask Source: https://recoupable.dev/ask Representation: Summary of the public page. Follow the source for the complete page. Ask questions in a conversation grounded in Recoup's public website content. The same assistant can help develop a workflow plan without requiring a separate conversation. It has no access to private customer records or authenticated platform tools. ## Next steps - [Ask Recoup](https://recoupable.dev/ask) - [Contact us](https://recoupable.dev/contact) ## AI readiness check for music companies Content ID: page:/audit Source: https://recoupable.dev/audit Representation: Summary of the public page. Follow the source for the complete page. The free readiness check asks about one recurring workflow, frequency, information, access, process documentation, ownership, and current AI use. No email is required. Recommendations focus on arranging information and access, making the job specific, identifying a workflow owner, or scoping a first build. It is a practical starting point, not a certification or predictive score. ## Next steps - [Take the readiness check](https://recoupable.dev/audit) - [AI advisory](https://recoupable.dev/advisory) ## AI workflow ROI planner Content ID: page:/roi Source: https://recoupable.dev/roi Representation: Summary of the public page. Follow the source for the complete page. Inputs include monthly task hours, hourly cost, expected time reduction, ongoing monthly system cost, and one-time setup cost. The planner calculates hours saved, capacity value, monthly net value, first-year value, and payback when applicable. Freed time is capacity, not automatically cash saved. Include preparation, review, correction, and maintenance; test conservative assumptions on real tasks before projecting results. ## Next steps - [Use the workflow planner](https://recoupable.dev/roi) - [Discuss the assumptions](https://recoupable.dev/contact) ## Read Recoup with your AI agent Content ID: page:/agents Source: https://recoupable.dev/agents Representation: Summary of the public page. Follow the source for the complete page. The public agent interface searches and reads the website's published content. Documentation, articles, and playbook chapters are returned as readable Markdown; marketing-page overviews are explicitly labeled as summaries with links to the complete pages. These tools do not access private account or client information, run platform tasks, submit inquiries, or book meetings. Platform actions use the separate authenticated Recoup API and MCP server. ## Next steps - [Agent access and setup](https://recoupable.dev/agents) - [Public documentation](https://recoupable.dev/docs) - [Authenticated platform tools](https://recoupable.dev/developers) ## Royalty reporting, organized. Content ID: page:/royalty-reporting Source: https://recoupable.dev/royalty-reporting Representation: Summary of the public page. Follow the source for the complete page. Built for labels, publishers, and catalog teams. Your files. Your rules. Start with the statements, receipts, and source mappings your team already uses. Agree on the comparison rules before building. A difference you can inspect. Compare by payment source, flag differences and timing items, and keep the supporting records beside each finding. A process you can run again. Rerun the workflow when inputs change. Keep unresolved items visible and leave materiality and sign-off with your team. Our royalty-reporting case study follows a working reconciliation workbook: overall totals, payment-source comparisons, flagged differences, and timing items. When an export changed, the team reran the review with the corrected input. An anonymized implementation. Time savings and recovered royalties have not been independently measured. Tell us how statements and receipts move through your team today. We’ll discuss the inputs, the report you need, and where human review belongs. Share your process in a few lines. We’ll follow up by email to arrange a conversation and work out whether a custom system makes sense. No files needed for this first conversation. We’ll agree on scope and price before work begins. ## Next steps - [Discuss my reporting workflow](https://recoupable.dev/royalty-reporting#inquiry) - [Royalty-reporting case study](https://recoupable.dev/case-studies/royalty-reporting) ## AI for music catalog acquisitions Content ID: page:/acquisitions Source: https://recoupable.dev/acquisitions Representation: Summary of the public page. Follow the source for the complete page. Recoup can build a workflow that gathers deal documents, financial models, and analyst notes into one review; traces findings to their sources; keeps missing information visible; and prepares questions for the seller. The result is a draft review for the investment team. The service supports preparation and review rather than promising an investment decision or guaranteed outcome. ## Next steps - [Acquisition workflow example](https://recoupable.dev/acquisitions#workflow) - [Discuss acquisition review](https://recoupable.dev/acquisitions/contact) ## AI royalty reporting and catalog operations Content ID: page:/operations Source: https://recoupable.dev/operations Representation: Summary of the public page. Follow the source for the complete page. Recoup can connect royalty statements, receipts, and catalog records; identify differences by payment source; and prepare a report with the underlying records and a focused review list. The example demonstrates why an overall matching total does not prove each source reconciles: offsetting differences can cancel out. Reviewers can inspect records behind individual differences. A project starts with the team's actual files, tools, and recurring reporting process. The scope defines the useful first build and review responsibilities. ## Next steps - [Royalty reporting example](https://recoupable.dev/operations#workflow) - [Discuss catalog operations](https://recoupable.dev/operations/contact) ## Recoup Privacy Policy Content ID: page:/privacy Source: https://recoupable.dev/privacy Representation: Summary of the public page. Follow the source for the complete page. The Privacy Policy describes information collection, usage, sharing, third-party services, security, individual rights and choices, children's privacy, international transfers, and policy changes. This is a navigation summary, not the complete policy. Consult the linked policy for its full text and stated update date. ## Next steps - [Read the complete Privacy Policy](https://recoupable.dev/privacy) ## Recoup Terms of Use Content ID: page:/terms Source: https://recoupable.dev/terms Representation: Summary of the public page. Follow the source for the complete page. The Terms of Use cover service access, registration, content responsibility, ownership, conduct, third-party services, purchases, subscriptions, warranties, liability, arbitration, and other provisions. This is a navigation summary, not the complete agreement. Read the full terms and any applicable supplemental terms before using the service. ## Next steps - [Read the complete Terms of Use](https://recoupable.dev/terms) ## Recoup project stories Content ID: page:/case-studies Source: https://recoupable.dev/case-studies Representation: Summary of the public page. Follow the source for the complete page. These stories describe documented work with music teams. Illustrations show the methods, not client financial data. They do not claim measured revenue gains or ROI. ## Next steps - [Royalty reports. With answers behind the numbers.](https://recoupable.dev/case-studies/royalty-reporting) - [A first draft that knows the deal.](https://recoupable.dev/case-studies/investment-review) - [A catalog brief worth opening.](https://recoupable.dev/case-studies/catalog-intelligence) ## Royalty reports. With answers behind the numbers. Content ID: page:/case-studies/royalty-reporting Source: https://recoupable.dev/case-studies/royalty-reporting Representation: Summary of the public page. Follow the source for the complete page. From chasing a difference to inspecting its source. Royalty statements and cash receipts tell different sides of the same story. Before reporting, the finance team needs to know which sources agree, which payments are still in transit, and which differences need attention. Checking only the grand total can miss the detail: an overpayment in one source can offset a shortfall in another. The review has to go deeper without becoming another spreadsheet rebuilt by hand. The system produces one workbook with overall totals, comparisons by payment source, flagged differences, and timing items. The finance team can inspect the records behind each finding and approve the review. When a source export was corrected, the workflow was run again with the updated input. The team could revise the report without rebuilding the process. Unresolved items stayed visible for the next review. An anonymized account of documented implementation and operating records. Business time savings and recovered royalties have not been independently measured. The illustration shows the method, not a client’s financial data. ## Next steps - [Explore the reporting example](https://recoupable.dev/operations#royalty-example) - [Discuss a similar project](https://recoupable.dev/contact) - [All project stories](https://recoupable.dev/case-studies) ## A first draft that knows the deal. Content ID: page:/case-studies/investment-review Source: https://recoupable.dev/case-studies/investment-review Representation: Summary of the public page. Follow the source for the complete page. From assembling the context to reviewing the draft. A model holds the assumptions. Previous reports establish the house format. Analyst notes capture the questions, and email threads contain details that never made it into the data room. Turning that material into a coherent diligence memo means reconstructing the context each time. A generic chat prompt starts too far away from the team’s actual work. The analyst used the workspace to prepare a diligence draft from prior reports, a model, notes, and email context. That draft became the starting point for their edits. The same workspace helped them work through questions about model changes. The information was already in context when the analyst began reviewing. Checking assumptions, resolving open questions, and making the investment decision stayed with the team. An anonymized account of documented operator use of a Recoup-enabled workspace. Deal names, model values, and customer quotations are kept private. Preparation time and investment outcomes have not been independently measured. ## Next steps - [Explore the investment example](https://recoupable.dev/acquisitions#acquisition-example) - [Discuss a similar project](https://recoupable.dev/contact) - [All project stories](https://recoupable.dev/case-studies) ## A catalog brief worth opening. Content ID: page:/case-studies/catalog-intelligence Source: https://recoupable.dev/case-studies/catalog-intelligence Representation: Summary of the public page. Follow the source for the complete page. From another data export to a useful review list. A catalog can generate movement across streaming, video, social, and discovery platforms. Some changes are meaningful. Others reflect duplicate records, stale sources, or a mismatch in reporting windows. The team needs a short list it can investigate, with enough context to distinguish an opportunity from a data problem. Recurring briefs bring period changes, related track movements, and source checks into one review. The workflow flags data anomalies alongside the items that deserve a closer look. The operator starts with a prepared brief and the questions to investigate. Choosing a campaign or acting on an opportunity stays with the team. An anonymized account of implemented workflows and repeated report outputs. Campaign impact and reader engagement have not been measured. The illustration contains no actual artist or catalog data. ## Next steps - [Explore artist and catalog tools](https://recoupable.dev/platform) - [Discuss a similar project](https://recoupable.dev/contact) - [All project stories](https://recoupable.dev/case-studies) ## Music Videos for Less Than $10 | Free Skill Download Content ID: page:/music-videos Download the free skill. Give your AI agent an idea, then shape the song, the scenes and the finished music video. Free download. No signup to get the skill. Under $10 is the self-serve generation budget. Plan fees and extra takes are separate. [Download skill](/downloads/recoup-music-video.zip) [Make it in Recoup](https://app.recoupable.dev) ## The workflow is yours. Get the instructions behind the process: plan the story, keep characters consistent, animate the scenes and check the final cut. A skill is a set of instructions for your AI agent. - Song, story and scene planning - Characters, locations and visual continuity - Image and motion prompts - Editing, quality checks and budget guidance [Read the skill on GitHub](https://github.com/recoupable/skills/tree/main/skills/recoup-music-video) ## Two songs. Two different worlds. Music videos we made with artists, built around their released recordings. ### Movamos el mundo · Tomás Mika Tomás had shot the original video alone in a field. He asked for a story with a couple, different places and a life together. The video ends in his field. [Watch the video on YouTube](https://www.youtube.com/watch?v=ouLTF_CsDGY) ### Letal Xlug · brauxelion ft. Shisosaloud The rain, neon and game world of his EP, carried into a video for his verse. Built around his recording and approved by the artist. [Watch the video on YouTube](https://www.youtube.com/watch?v=f_GElz8Xc_A) ## From an idea to a finished music video. ### Get the skill Download it for your AI agent, or start in Recoup with the workflow connected. ### Shape the song and story Start with a new song. Choose the look, characters and scenes before spending on animation. ### Make the video Set the budget, review each scene, then render your video. Rework only what needs another take. ### Already have a released track? The downloadable skill currently starts with a generated song. For a video using your own recording, [work with our team](/music-videos#request). ## Your recording. Our help making the video. Send your released song and the world you picture. We agree on scope and a custom quote before production begins. Custom production is quoted separately from the self-serve budget. - A visual direction shaped with you - A scene plan and images to review - An edited video set to your recording - A final cut for you to approve and publish [Get a quote for my song](/music-videos#request) ## A few things to know. ### Is the skill really free? Yes. Download the skill and its reference guide without an email address or Recoup account. Generating songs, images and video uses paid credits. ### What does “less than $10” cover? The self-serve generation budget for a music video. Length, resolution and extra takes affect the total. Recoup plan fees and custom production are separate. ### Can I use my own song? The downloadable skill currently generates a new song. If you want a video for a released recording, send it to our team for a custom quote. ### What do I need to use the skill? An AI agent that can follow the instructions and access Recoup’s generation tools. The file is free; running it requires a Recoup account and generation credits. You can also start directly in Recoup. ### Does the free download include video generation? The skill is free to download. Running it requires a Recoup account and generation credits. The published workflow describes a 15-second starter piece and full-length music videos; available duration depends on your account access. Check your plan before generating. ## Make the video you picture. Start with the free skill. See where the song takes you. [Download skill](/downloads/recoup-music-video.zip) ## Read the complete topic [Search published content](https://recoupable.dev/agent-api/v1/search?query=royalty&type=all&limit=5), then pass a returned ID to https://recoupable.dev/agent-api/v1/read. A response states its representation and provides a continuation offset when applicable. [Tool and content catalog](https://recoupable.dev/agents/catalog.json) | [Documentation](https://recoupable.dev/docs) | [Website OpenAPI](https://recoupable.dev/openapi.json) ## Access and boundaries - This website's /agent-api/v1 endpoints are public. They search and read published content, calculate from supplied assumptions, and return inquiry drafts. They do not access private catalogs or operate a Recoup account. - The platform REST API is separate: https://api.recoupable.dev/api. Programmatic access normally uses x-api-key. Some platform endpoints support a Privy access token as Authorization: Bearer instead; do not send both headers. Follow the endpoint's documentation and [authentication guide](https://recoupable.dev/docs/authentication). - The platform MCP server is https://api.recoupable.dev/mcp. Its connection guide uses Authorization: Bearer with a Recoup API key. See [MCP documentation](https://recoupable.dev/docs/mcp). This website does not host a remote MCP server. - Account creation, messages, uploads, connector actions, and other platform writes are separate operations with their own permissions. A documentation example is not authorization to execute them. - Website tools do not submit an inquiry or book a call. A prepared brief is a draft for review; the visitor completes the [contact form](https://recoupable.dev/contact). - Workflow ROI uses supplied assumptions. Capacity value is not guaranteed cash savings, a quote, or a promise of results. - Never send credentials or private client records to public website search or tools. # Recoup API Documentation Source: https://recoupable.dev/docs Use the Recoup API to build your record label. Access research, content creation, chat, artist analytics, social media, and platform management endpoints. ## Connect your account For an AI host, start with [MCP OAuth](https://recoupable.dev/docs/mcp-oauth): sign in to your real account, approve the intended scope and verify access with a read. For REST integrations, create an API key in your [account settings](https://chat.recoupable.dev/keys), or follow the [verified email/PIN flow](https://recoupable.dev/docs/agents). Keep credentials in a secret facility. An empty roster can be valid; do not create a disposable account to recover access. ## What is Recoup? Recoup is an AI agent platform for smarter song rollouts, unforgettable fan experiences, and lasting artist growth. Empowering music executives with actionable insights and next-gen tools. This is where record labels, musicians, and managers start to build on Recoup AI technology like chat, tasks, agents, and more. ## Base URL All API requests should be made to: ```bash https://api.recoupable.dev/api ``` ## Authentication Most API endpoints are authenticated using an API key passed in the `x-api-key` header. The only exceptions are the [agent onboarding](https://recoupable.dev/docs/agents) endpoints — [`POST /api/agents/signup`](https://recoupable.dev/docs/api-reference/agents/signup) and [`POST /api/agents/verify`](https://recoupable.dev/docs/api-reference/agents/verify) — which are intentionally unauthenticated so agents can obtain their first API key. 1. Navigate to the [API Keys Management Page](https://chat.recoupable.dev/keys) 2. Sign in with your account 3. Create a new API key and copy it immediately (it's only shown once) ```bash curl -X GET "https://api.recoupable.dev/api/artists?accountId=YOUR_ACCOUNT_ID" \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" ``` > **Warning** > Keep your API key secure. Do not share it publicly or commit it to version control. ## Get Started [Agent Signup](https://recoupable.dev/docs/agents) Verify your email and obtain a separate REST API key. [Quickstart](https://recoupable.dev/docs/quickstart) Get your API key and make your first request in minutes. [CLI](https://recoupable.dev/docs/cli) Install and use the Recoup CLI to interact with the platform from your terminal. [Claude](https://recoupable.dev/docs/claude) Connect Claude to Recoup through MCP OAuth, then add optional workflow skills. [MCP Server](https://recoupable.dev/docs/mcp) Connect Recoup to AI assistants via the Model Context Protocol. ## API Sections The API includes these major sections. Use these links to jump to the right area. [Research](https://recoupable.dev/docs/api-reference/research/search) Recoup artist and track discovery, current metrics, activities and playlists; web research, upcoming events, and stored play-count measurements. [Content Creation](https://recoupable.dev/docs/api-reference/content/create) Generate images, videos, and captions. Transcribe audio, edit content, upscale media, analyze videos, manage templates, and estimate costs. [Chat](https://recoupable.dev/docs/api-reference/chat/chats) Conversations with artist context. Create, stream, and generate messages. Copy messages, delete trailing messages, and manage chat history. [Social Media](https://recoupable.dev/docs/api-reference/connectors/list) Spotify, Instagram, X (Twitter), and generic social scraping. Search artists, scrape profiles and comments, track trends, and manage OAuth connectors. [Releases](https://recoupable.dev/docs/api-reference/songs/songs) Songs, catalogs, and task management. Analyze songs, manage catalog collections, and schedule recurring tasks with cron-based automation. [Accounts](https://recoupable.dev/docs/api-reference/pulses/update) Accounts, organizations, subscriptions, pulses, notifications, sandboxes, and admin tools. [Release Players](https://recoupable.dev/docs/essentials/release-player) Hosted listening pages, embeddable players, event reporting and fan connections. ## Agents [Content Agent](https://recoupable.dev/docs/content-agent) Content creation agent accessible via Slack. Generates images, videos, and captions for artists automatically. [Authentication](https://recoupable.dev/docs/authentication) API key authentication, account-scoped access, and organization-level permissions. ## Quick Reference for LLMs If you are an LLM navigating these docs, here is a summary of the endpoint structure: - **`/api/artists/*`** — Artist management (list, create, socials, socials-scrape, profile) - **`/api/research/*`** — Artist and track search, lookup, profile, current metrics, audience, related artists, URLs, playlists, catalog entries and activities; track statistics and measurements; web, deep and people research, extraction, enrichment and events. See each operation's contract for provider mapping and coverage limits. - **`/api/content/*`** — Content creation (create, generate-image, generate-video, generate-caption, transcribe-audio, edit, upscale, analyze-video, templates, validate, estimate) - **`/api/chat/*`** — Chat (chats, artist, messages, messages-copy, messages-trailing-delete, create, update, delete, runs, runs-status, stream, compact) - **`/api/songs/*`** — Songs and catalogs (songs, create, analyze, analyze-presets, catalogs, catalogs-create, catalogs-delete, catalog-songs, catalog-songs-add, catalog-songs-delete) - **`/api/tasks/*`** — Task automation (get, create, update, delete, runs) - **`/api/spotify/*`** — Spotify (search, artist, artist-albums, artist-top-tracks, album) - **`/api/instagram/*`** — Instagram (comments, profiles) - **`/api/x/*`** — X/Twitter (search, trends) - **`/api/connectors/*`** — OAuth connectors (list, authorize, disconnect) - **`/api/accounts/*`** — Accounts (get, id, create, update, add-artist) - **`/api/organizations/*`** — Organizations (list, create, add-artist) - **`/api/sandboxes/*`** — Sandboxes (list, create, snapshot, delete, setup, file, upload) - **`/api/players/*`** — Release Players (list, create, update, activity and fans) - **`/api/content-agent/*`** — Content agent webhooks (webhook, callback) - **`/api/agents/*`** — Agent onboarding (signup, verify) — no auth required Base URL: `https://api.recoupable.dev/api` [OpenAPI Specification](https://github.com/recoupable/marketing/tree/main/content/docs/source/api-reference/openapi) ## Need Help? [Contact Support](mailto:agent@recoupable.dev) Reach out to our team at [agent@recoupable.dev](mailto:agent@recoupable.dev) for assistance with the Recoup API. # Quickstart Source: https://recoupable.dev/docs/quickstart Connect your real account and make your first authenticated request. ## Connect first Use [MCP OAuth](https://recoupable.dev/docs/mcp-oauth) for a supported AI host. For the REST examples below, create a personal API key in [account settings](https://chat.recoupable.dev/keys) or follow the [Agents email verification guide](https://recoupable.dev/docs/agents). Store the key as `RECOUP_API_KEY` through your secret facility or an approved environment file. Never paste credentials into agent instructions or create a disposable account to access an existing customer's data. Verify the REST credential before continuing: ```bash curl --fail-with-body -sS -H "x-api-key: $RECOUP_API_KEY" \ https://api.recoupable.dev/api/accounts/id ``` ## Base URL All API requests should be made to: ```bash https://api.recoupable.dev/api ``` ## Your First Request Once you have an API key, include it in the `x-api-key` header on every request. Here's a simple call that retrieves your scheduled tasks: ```bash cURL curl -X GET "https://api.recoupable.dev/api/tasks" \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" ``` ```python Python import requests headers = { "Content-Type": "application/json", "x-api-key": "YOUR_API_KEY" } response = requests.get( "https://api.recoupable.dev/api/tasks", headers=headers ) print(response.json()) ``` ```javascript JavaScript const response = await fetch("https://api.recoupable.dev/api/tasks", { headers: { "Content-Type": "application/json", "x-api-key": "YOUR_API_KEY", }, }); const data = await response.json(); console.log(data); ``` ```typescript TypeScript interface Task { id: string; title: string; prompt: string; schedule: string; account_id: string; artist_account_id: string; enabled: boolean; } interface TasksResponse { status: "success" | "error"; tasks: Task[]; } const response = await fetch("https://api.recoupable.dev/api/tasks", { headers: { "Content-Type": "application/json", "x-api-key": "YOUR_API_KEY", }, }); const data: TasksResponse = await response.json(); console.log(data.tasks); ``` **Example Response:** ```json { "status": "success", "tasks": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "title": "Daily Fan Report", "prompt": "Generate a summary of new fans from the past 24 hours", "schedule": "0 9 * * *", "account_id": "123e4567-e89b-12d3-a456-426614174000", "artist_account_id": "987fcdeb-51a2-3b4c-d5e6-789012345678", "enabled": true } ] } ``` > **Info** > For full documentation on the Tasks API including filtering options, see the [Tasks API Reference](https://recoupable.dev/docs/api-reference/tasks/get). ## Prefer the dashboard? If you're a human building an integration, you can also create API keys from the web console instead of the signup endpoint: 1. Navigate to the [Recoup API Keys Management Page](https://chat.recoupable.dev/keys) 2. Sign in with your account 3. Enter a descriptive name (e.g. "Production Server") 4. Click **Create API Key** > **Warning** > Copy and securely store your API key immediately — it will only be shown once. ## Next Steps With your API key ready, you can now: [Artist Data](https://recoupable.dev/docs/api-reference/artists/socials) Fetch artists' social accounts and follower counts. [Fan Analytics](https://recoupable.dev/docs/api-reference/fans/get) Access fan data across all connected social platforms. [Chat API](https://recoupable.dev/docs/api-reference/chat/chats) Build AI-powered conversations with artist context. [Task Management](https://recoupable.dev/docs/api-reference/tasks/get) Schedule and automate recurring tasks. ## Support If you need help or have questions about the API, please contact our support team at [agent@recoupable.dev](mailto:agent@recoupable.dev). # CLI Source: https://recoupable.dev/docs/cli Install the Recoup CLI and interact with the platform from your terminal. The Recoup CLI (`@recoupable/cli`) wraps the Recoup API for terminal-first workflows. It's available as a global npm package and comes pre-installed in sandbox environments. > **Note** > **The CLI is in beta.** The reference below follows the v0.1.15 source command set. Check `recoup --version` and `--help` for the package installed in your environment. For research workflows and most content operations, call the [REST API](https://recoupable.dev/docs/api-reference) directly — the CLI is gradually catching up. > > **Agents:** if a command isn't listed below, don't retry — fall back to the corresponding REST endpoint linked under each command. ## Install ```bash npm install -g @recoupable/cli ``` ## Authenticate Set your API key as an environment variable: ```bash export RECOUP_API_KEY=your-api-key ``` Verify it works: ```bash recoup whoami ``` > **Tip** > Get an API key from the [API Keys page](https://chat.recoupable.dev/keys) or use the [agent signup](https://recoupable.dev/docs/agents) for instant key generation. ## Configuration | Variable | Required | Default | Description | | --- | --- | --- | --- | | `RECOUP_API_KEY` | Yes | — | Your Recoup API key | | `RECOUP_API_URL` | No | `https://recoup-api.vercel.app` | API base URL override; set `https://api.recoupable.dev` to use the canonical API domain | All commands support `--json` for machine-readable output and `--help` for usage info. --- ## Commands ### whoami Show the authenticated account. See [`GET /api/accounts/id`](https://recoupable.dev/docs/api-reference/accounts/id). ```bash recoup whoami recoup whoami --json ``` ### orgs List organizations. See [`GET /api/organizations`](https://recoupable.dev/docs/api-reference/organizations/list). ```bash recoup orgs list recoup orgs list --account ``` ### artists List artists. See [`GET /api/artists`](https://recoupable.dev/docs/api-reference/artists/list). ```bash recoup artists list recoup artists list --org recoup artists list --account ``` ### emails Send an email to the authenticated account. See [`POST /api/emails`](https://recoupable.dev/docs/api-reference/emails/create). ```bash recoup emails --subject "Pulse Report" --text "Here's your weekly summary." recoup emails --subject "Update" --html "

Report

Details here.

" ``` | Flag | Required | Description | | --- | --- | --- | | `--subject ` | No | Email subject line; defaults from the body when omitted | | `--text ` | No | Plain text or Markdown body | | `--html ` | No | Raw HTML body (takes precedence over `--text`) | | `--cc ` | No | CC recipient (repeatable) | | `--chat-id ` | No | Chat ID for a chat link in the email footer | | `--account ` | No | Send to a specific account (org keys only) | ### chats Manage chats. See [`GET /api/chats`](https://recoupable.dev/docs/api-reference/chat/chats) and [`POST /api/chats`](https://recoupable.dev/docs/api-reference/chat/create). ```bash recoup chats list recoup chats create --name "My Topic" recoup chats create --name "My Topic" --artist ``` ### sandboxes Manage sandboxes. See [`GET /api/sandboxes`](https://recoupable.dev/docs/api-reference/sandboxes/list) and [`POST /api/sandboxes`](https://recoupable.dev/docs/api-reference/sandboxes/create). ```bash recoup sandboxes list recoup sandboxes create recoup sandboxes create --command "ls -la" ``` ### tasks Check background task status. See [`GET /api/tasks/runs`](https://recoupable.dev/docs/api-reference/tasks/runs). ```bash recoup tasks status --run ``` ### songs Run AI music analysis. See [`POST /api/songs/analyze`](https://recoupable.dev/docs/api-reference/songs/analyze) and [`GET /api/songs/analyze/presets`](https://recoupable.dev/docs/api-reference/songs/analyze-presets). ```bash recoup songs presets recoup songs analyze --preset catalog_metadata --audio https://example.com/track.mp3 recoup songs analyze --prompt "Describe the production style" --audio https://example.com/track.mp3 ``` Exactly one of `--preset` or `--prompt` is required, together with `--audio`. The API rejects both selectors together and verifies the audio URL before inference. The coordinated CLI validation fix reports these errors before sending a request; older installed packages may send them to the API. | Flag | Required | Description | | --- | --- | --- | | `--preset ` | Conditional | Curated analysis preset. Required when `--prompt` is omitted. | | `--prompt ` | Conditional | Custom text prompt. Required when `--preset` is omitted. | | `--audio ` | Yes | Public URL to an audio file (MP3, WAV, FLAC), required for every preset and custom prompt. Metadata-only analysis is not supported. | | `--max-tokens ` | No | Max tokens to generate (default 512). | --- ## content Content creation pipeline — generate AI-powered social videos for artists. ### List templates ```bash recoup content templates ``` ### Validate an artist Check that an artist has the required assets before creating content. See [`GET /api/content/validate`](https://recoupable.dev/docs/api-reference/content/validate). ```bash recoup content validate --artist ``` ### Estimate cost Preview estimated cost and duration before kicking off the pipeline. See [`GET /api/content/estimate`](https://recoupable.dev/docs/api-reference/content/estimate). ```bash recoup content estimate recoup content estimate --lipsync --batch 2 --compare ``` ### Create content Run the full content-creation pipeline for an artist. See [`POST /api/content/create`](https://recoupable.dev/docs/api-reference/content/create). ```bash recoup content create --artist recoup content create --artist --template --lipsync --upscale ``` | Flag | Required | Description | | --- | --- | --- | | `--artist ` | Yes | Artist account ID | | `--template ` | No | Template name (default: random) | | `--lipsync` | No | Enable lipsync mode | | `--upscale` | No | Enable upscaling | | `--caption-length ` | No | Max caption length in characters | > **Tip** > For finer-grained control (individual image, video, caption, transcription, edit, upscale, or analyze operations), call the [content REST endpoints](https://recoupable.dev/docs/api-reference/content/generate-image) directly. Those primitives aren't yet exposed as individual CLI subcommands. # Claude Source: https://recoupable.dev/docs/claude Connect Claude to Recoup and install optional workflow instructions. Add `https://api.recoupable.dev/mcp` in Claude's remote connector controls and complete browser OAuth using your real Recoup account. See [MCP OAuth](https://recoupable.dev/docs/mcp-oauth) for the dated client verification record and troubleshooting. After connecting, refresh tools and call the discovered `list_artists` tool. Report success after that read succeeds; a legitimate empty roster does not require creating another account. For workflow instructions, install the consolidated Recoup package from the [Recoup marketplace](https://github.com/recoupable/marketplace), using the manifest/package name shown there. Skills-only installation with `npx skills add recoupable/skills` does not configure MCP or create credentials. Standalone platform/content/research packages remain separate distributions; their installed versions may contain older instructions until their reconciliation release is installed. MCP OAuth credentials stay in Claude. A REST-only script needs a separately configured API key or application-provided access token. Use a supported secret facility or a user-approved environment file; never paste keys into Profile or Global instructions. Do not register a disposable account to recover customer access. Start a fresh conversation and verify tool discovery and artist access again. Browser sign-in, plugin installation and token refresh are separate checks; none alone proves the complete workflow. # MCP Source: https://recoupable.dev/docs/mcp Connect an agent to Recoup through OAuth and discover its authorized tools. Connect your host to `https://api.recoupable.dev/mcp` using OAuth. Sign in to your real Recoup account and approve the requested scope. Follow the [OAuth setup guide](https://recoupable.dev/docs/mcp-oauth). Use `tools/list` after connection and after changing consent. The catalog depends on the grant, credential type and deployed server. A fixed count is not a compatibility contract. For example, discover `list_artists` and call it with its returned schema to verify access. An empty roster is a valid result. `mcp:tools` grants the business catalog, including writes and paid generation. Narrow grants remain restricted until broader consent is approved. Account membership determines accessible personal and organization data; OAuth consent does not create organization access. Sandbox command tools previously shown on this page are absent from the current registry. Do not call `prompt_sandbox`, `run_sandbox_command`, or infer that every REST endpoint is exposed as MCP. Use documented [REST sandbox operations](https://recoupable.dev/docs/api-reference/sandboxes/create) with a separate authorized credential when required. For an API-key MCP connection, configure `Authorization: Bearer YOUR_RECOUP_API_KEY` in the client's secret settings. The `x-api-key` header is for [REST requests](https://recoupable.dev/docs/authentication). Store credentials in the client's secret facility or environment. Never paste them into global instructions or extract host-managed OAuth tokens for REST. # Connect an agent Source: https://recoupable.dev/docs/mcp-oauth Connect an MCP client to Recoup with OAuth and account-authorized tools. Use this URL in your agent's remote MCP settings: ```text https://api.recoupable.dev/mcp ``` Select **OAuth**. Recoup supports [published client metadata (CIMD)](https://recoupable.dev/docs/mcp-cimd) and **automatic registration** (also called Dynamic Client Registration or DCR). Sign in to your existing Recoup account and allow the requested access. No API key or client secret needs to be copied. ## What your agent can do Request **`mcp:tools`** for the full catalog: a scope-dependent catalog covering artists and socials, catalogs, chats, scheduled tasks, research/search, Spotify, YouTube revenue, image/audio/video operations, files, Context, Sites, email and support. The consent screen explains that this permission includes reading, creating, editing and deleting data, sending messages, publishing content, and using paid generation tools. It covers personal and organization workspaces your account can access. Resource ownership is checked on each call. Private conversations and scheduled tasks remain account-bound. | Permission | Access | | --- | --- | | `mcp:tools` | Full business-tool catalog across accessible workspaces | | `mcp:read` | Original personal-only `list_artists`, `get_artist_socials`, `get_chats` | | `mcp:write` | Original personal-only `create_new_artist`, `update_account_info` | Existing limited grants do **not** expand automatically. Reconnect with `mcp:tools`, review the broader permission, and refresh your client's tool catalog. Clients requesting the `mcp:read` + `mcp:write` pair keep the five-tool experience. OAuth never exposes `get_api_key`: it does not return bearer credentials to the model. Composio integrations and sandbox execution are separate from the registered Recoup MCP catalog. Email uses the same recipient restrictions as the API and checks any referenced conversation's owner. Review recipients and content before sending. `create_knowledge_base` and `generate_txt_file` publish to permanent public Arweave storage: use only content explicitly approved for that destination, never confidential material. Video generation returns an account-bound `id`; pass that entire value as `video_id` to the retrieval tools. Older raw provider video IDs cannot be used with OAuth. Task-run status requires verifiable caller ownership and returns status fields, not raw provider payloads or logs. The platform verification results below describe the original five-tool catalog unless explicitly stated otherwise. They do not establish full-catalog production verification. New connections stay authorized until you disconnect them, including when inactive. Five-minute access tokens renew through rotating refresh tokens. Disconnect an agent from [Connected agents](https://app.recoupable.dev/oauth/connections) to revoke its access and refresh tokens immediately. Connections approved under the previous 30-day policy keep their original expiration. Reconnect once to approve access until disconnected. ## Claude 1. Open **Customize → Connectors → Add custom connector**. 2. Name it **Recoup** and enter the MCP URL above. 3. Choose **Sign in now**. **Register automatically** uses DCR; **published identity** uses CIMD. The production client tests below used DCR. 4. Connect, sign in to Recoup, and select **Allow access**. 5. In a new chat, ask Claude to list your Recoup artists. Review individual tool-use approvals when prompted. Claude web was tested against production on October 7, 2026: connection, five-tool discovery, artist read, temporary artist creation/update/readback, artist socials, and another read after access-token expiry passed. The temporary artist was removed afterward. Claude Desktop 2.26454.0 was also tested on October 7, 2026 using the same authorized connector: tool discovery, artist listing, temporary artist creation/update/readback passed. The test artist was removed afterward. Separate disconnect/reconnect checks are still pending. Local stdio configuration is a different transport. See [Claude's remote connector guide](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp). ## ChatGPT In the interface tested on October 7, 2026: 1. Open **Plugins → Add → Add custom MCP server**. 2. Enter **Recoup**, the MCP URL, and **OAuth**. 3. Under advanced OAuth settings, confirm **Dynamic Client Registration (DCR)** and request `mcp:tools` for full access. 4. Create the plugin, connect it, and complete Recoup consent. 5. Start a chat with Recoup enabled and request an artist list. ChatGPT web was tested against production on October 7, 2026: OAuth login, artist listing, and temporary artist creation/update/readback passed. The temporary artist was removed afterward. Refresh and disconnect/reconnect checks are pending. Workspace policy and account access may limit custom integrations. UI labels can differ; see [OpenAI's MCP setup documentation](https://help.openai.com/en/articles/12584461-developer-mode-and-mcp-apps-in-chatgpt). ## Codex ```bash codex mcp add recoup --url https://api.recoupable.dev/mcp codex mcp login recoup --scopes mcp:tools ``` Complete the browser sign-in and consent, then ask Codex to list your Recoup artists. Codex CLI 0.161.0 passed production OAuth login and an actual `list_artists` call on October 7, 2026. Version 0.145.0 failed issuer-response validation; upgrade if using that version. Writes, refresh, and disconnect/reconnect remain unverified in Codex. Desktop and IDE surfaces must be verified separately. See [Codex MCP configuration](https://developers.openai.com/codex/mcp). ## Cursor Add a remote server to your project's `.cursor/mcp.json`: ```json { "mcpServers": { "recoup": { "url": "https://api.recoupable.dev/mcp" } } } ``` Open Cursor's MCP settings and authenticate. Cursor Desktop passed production OAuth connection, five-tool discovery, and an actual `list_artists` call on October 7, 2026. Writes, refresh, disconnect/reconnect, and Cursor web/agent journeys remain unverified. See [Cursor's MCP guide](https://cursor.com/docs/mcp) for the current setup and workspace restrictions. ## Other agents and callbacks Use a remote Streamable HTTP MCP client supporting OAuth discovery, authorization code flow with PKCE S256, and DCR. Recoup publishes: - Resource metadata: `https://api.recoupable.dev/.well-known/oauth-protected-resource/mcp` - Authorization-server metadata: `https://api.recoupable.dev/.well-known/oauth-authorization-server/api/oauth` - Resource: `https://api.recoupable.dev/mcp` Register the exact callback used by your client. Registration binds callbacks to that client; there is no shared wildcard callback list. Hosted callbacks use HTTPS. Native clients use validated loopback callbacks. Do not substitute `localhost` for `127.0.0.1` or edit a callback after registration. A generic MCP SDK client has passed production reads, writes, refresh rotation, and revocation. That does not prove every agent surface. CIMD uses the same consent and permissions; see [CIMD requirements and verification status](https://recoupable.dev/docs/mcp-cimd). Full-tool consent includes accessible organization workspaces; the original limited grants remain personal-only. ### Additional client setup paths The following paths were reviewed in official client documentation on October 7, 2026. **They have not been tested end to end against Recoup.** Use automatic registration and request `mcp:tools` for full access, or `mcp:read` / `mcp:write` for the limited personal tools. The client registers its callback; you do not need a Recoup platform-specific callback allowlist. | Client | Setup and callback guidance | | --- | --- | | [Perplexity](https://www.perplexity.ai/help-center/en/articles/13915507-adding-custom-remote-connectors) | Add a custom remote connector with OAuth and DCR. Use per-user authentication. Documented callbacks are `https://www.perplexity.ai/rest/connections/oauth_callback` and, for enterprise, `https://enterprise.perplexity.ai/rest/connections/oauth_callback`. | | [Claude Code](https://code.claude.com/docs/en/mcp) | Add Recoup as an HTTP server and authenticate through `/mcp` inside Claude Code. Automatic registration uses a localhost callback; `--callback-port` can fix its port when required. | | [VS Code](https://code.visualstudio.com/api/extension-guides/ai/mcp#authorization) | Add an HTTP server in `.vscode/mcp.json`. OAuth supports DCR; documented callbacks include `http://127.0.0.1:33418` and `https://vscode.dev/redirect`. Preserve the callback supplied by your VS Code surface. | | [Gemini CLI](https://geminicli.com/docs/tools/mcp-server/) | Configure the MCP URL as `httpUrl` and authenticate with `/mcp auth recoup` inside the Gemini CLI chat. DCR uses `http://localhost:/oauth/callback` unless configured otherwise. | | [OpenCode](https://opencode.ai/docs/mcp-servers/) | Add a remote MCP server and run `opencode mcp auth recoup`. OAuth supports automatic registration. Let the client supply its callback. | | [Kiro](https://kiro.dev/docs/mcp/configuration/) | Add the remote MCP URL and use DCR. Set `oauth.oauthScopes` to Recoup's scopes; the default identity scopes do not grant Recoup tool access. Use the client's loopback callback. | | [n8n](https://docs.n8n.io/integrations/builtin/credentials/mcp/) | Create an MCP OAuth credential with **Use Dynamic Client Registration** enabled. Use the exact callback shown by your n8n instance. | | [Copilot Studio](https://learn.microsoft.com/en-us/microsoft-copilot-studio/mcp-add-existing-server-to-agent) | Add an MCP tool using Streamable HTTP and OAuth dynamic discovery/registration. If setup asks for a callback, use the exact URL shown by Copilot Studio. | Documented compatibility is a setup starting point. A successful connection still needs an actual tool call before it can be considered verified. Refresh and disconnect/reconnect should be checked before relying on unattended access. ## Disconnect Open [Connected agents](https://app.recoupable.dev/oauth/connections) and disconnect the selected agent. Future requests and refreshes stop; already completed changes remain. Other connections are independent. Reconnect from the agent when you want to grant access again. ## Troubleshooting | Symptom | What to do | | --- | --- | | Custom connector option missing | Check your client's plan and workspace-admin permissions. | | Published identity / CIMD unavailable or fails | Check the [metadata requirements](https://recoupable.dev/docs/mcp-cimd), or choose automatic registration / DCR if your client offers it. | | Connection request expired | Restart Connect from the client; do not reuse an old consent URL. | | Wrong Recoup account | Use Switch before approving. | | Still seeing only five tools | Reconnect requesting `mcp:tools`, approve full access, then refresh the client tool catalog. | | Connected but no tool access | Check selected scopes, individual tool approvals, and whether the grant expired or was revoked. | | Organization artist missing | Reconnect with `mcp:tools` and verify your organization membership. | | `401` after disconnect | Expected: reconnect and approve a new grant. | | `429` | Respect `Retry-After`; avoid repeated registration attempts. | | `503` | Retry later; do not weaken authentication or callback checks. | | Write timed out | Read back the target before retrying. Artist creation is not guaranteed idempotent; an automatic retry may create a duplicate. | For clients without OAuth, [API-key authentication](https://recoupable.dev/docs/mcp) remains available. Keep keys out of model prompts and shared configuration. A stdio-only client needs a separately maintained adapter; the remote URL is not itself a stdio command. For help, contact [agent@recoupable.dev](mailto:agent@recoupable.dev). Include the client name, time, and error text; never include tokens, passwords, or API keys. Full OAuth MCP calls currently share account-level limits of 120 calls per minute and 30 calls per tool per minute across connected clients. See the [pinned rate-limit implementation](https://github.com/recoupable/api/blob/c25715ea1b5e31ac31d0079955b90dc1ce2f1631/lib/mcp/oauth/checkFullOAuthRateLimit.ts). The `compact_chats` MCP tool accepts up to 50 chats per call under the [pinned MCP guard](https://github.com/recoupable/api/blob/c25715ea1b5e31ac31d0079955b90dc1ce2f1631/lib/mcp/oauth/prepareFullOAuthTool.ts); this is an MCP limit, not a claimed REST request limit. # Published client metadata (CIMD) Source: https://recoupable.dev/docs/mcp-cimd Connect using an HTTPS client identity without dynamic registration. Recoup supports Client ID Metadata Documents (CIMD) alongside Dynamic Client Registration (DCR). A CIMD client uses the HTTPS URL of its public metadata document as its OAuth `client_id`. Recoup retrieves that document and validates the client's callbacks before asking you to approve access. Use the same MCP server URL: `https://api.recoupable.dev/mcp`. A compatible client can discover CIMD through `client_id_metadata_document_supported: true` in Recoup's [authorization-server metadata](https://api.recoupable.dev/.well-known/oauth-authorization-server/api/oauth). No vendor-specific registration or callback allowlist is needed. ## Access and trust CIMD uses the same [permissions and consent](https://recoupable.dev/docs/mcp-oauth) as DCR. It does not bypass sign-in, PKCE, tool scope checks, grant expiry, or disconnection. Existing DCR connections continue to work. A client name or logo is self-declared metadata, not proof of vendor endorsement. Check the client identity and requested permissions before allowing access. Publishing a document does not authorize its client to access your account. ## Client requirements - Serve JSON with HTTP 200 over public HTTPS on port 443. Redirects are rejected. - Set `client_id` to the exact document URL and include `client_name` and `redirect_uris`. - Use a document URL with an explicit path and without credentials, fragments, or dot path segments. - Use authorization code flow with PKCE S256 and `mcp:tools` for the full Recoup catalog, or `mcp:read` / `mcp:write` for the original limited personal tools. - Public clients should set `token_endpoint_auth_method` to `none`. Never publish client secrets or private keys. - Register exact callbacks. Hosted HTTP callbacks are rejected; native loopback callbacks support dynamic ports. `localhost` and `127.0.0.1` remain different hosts. Recoup limits each metadata/JWKS response to 16 KiB and each fetch to 2.5 seconds, including DNS resolution and body transfer. It rejects private, loopback, reserved, and mixed public/private DNS results and pins connections to a validated address. Remote JWKS retrieval follows the same network policy. Metadata caching respects freshness headers up to five minutes. `no-store`, `no-cache`, absent freshness information, or expired freshness causes a new fetch. A failed refresh does not reuse expired metadata. ## Verification status The implementation has passed local production-provider tests for CIMD discovery, consent, token exchange, MCP access, refresh rotation, and revoked access. Network tests cover unsafe destinations, redirects, timeouts, response sizes, and concurrency limits. A local smoke test also retrieved the official Claude and Claude Code documents over real DNS/TLS, resolved them using the production provider, and checked Claude Code's dynamic loopback ports. This is metadata compatibility evidence, not a completed production user login through CIMD. The client-specific production results on [Connect an agent](https://recoupable.dev/docs/mcp-oauth) were obtained using DCR. Production verification on October 7, 2026 confirmed CIMD discovery with DCR still available and successfully started OAuth authorization interactions using the official Claude and Claude Code client IDs, including a dynamic localhost callback port. Completing user sign-in and a tool call through CIMD in production remains unverified. If a metadata host is unavailable or blocks server requests, CIMD fails closed. Use DCR if the client offers that choice, or retry after its metadata endpoint is restored. See the [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization) and [CIMD draft 02](https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-02.html). CIMD remains a draft specification; Recoup's provider currently implements draft 02. # Authentication Source: https://recoupable.dev/docs/authentication How authentication works in the Recoup API — API keys, access tokens, and organization access control. ## Overview Every request to the Recoup API must be authenticated using exactly one of two mechanisms: | Method | Header | Use case | | --- | --- | --- | | API Key | `x-api-key` | Server-to-server integrations | | Access Token | `Authorization: Bearer ` | Frontend apps authenticated via Privy | Providing both headers in the same request will result in a `401` error. > **Note** > Agent onboarding endpoints (`POST /api/agents/signup` and `POST /api/agents/verify`) are **unauthenticated** — they exist so agents can obtain their first API key. See the [Agents guide](https://recoupable.dev/docs/agents) for details. --- ## API Keys API keys are the primary way to authenticate programmatic access to the Recoup API. All API keys are **personal keys** — they are always tied to the account that created them. ### Creating an API Key 1. Navigate to [chat.recoupable.dev/keys](https://chat.recoupable.dev/keys) 2. Enter a descriptive name (e.g. `"Production Server"`) 3. Click **Create API Key** > **Warning** > Copy your API key immediately — it is only shown once. Keys are stored as a secure HMAC-SHA256 hash and cannot be retrieved after creation. ### Using an API Key Pass your key in the `x-api-key` header: ```bash curl -X GET "https://api.recoupable.dev/api/tasks" \ -H "x-api-key: YOUR_API_KEY" ``` ### Access to Organizations If your account belongs to one or more organizations, your API key can access data across those organizations by passing an `account_id` parameter on supported endpoints. This lets you filter to any account within an organization your key has access to. - **No org membership** — the key can only access its own account's data - **Org member** — the key can pass `account_id` to filter to any account within that organization > **Info** > Org membership is determined by the account's [organizations](https://recoupable.dev/docs/api-reference/organizations/list). An account gains access to an org when it is added as a member. --- ## Access Tokens (Privy) If you're building a frontend application that authenticates users via [Privy](https://privy.io/), you can pass the user's Privy JWT as a Bearer token instead of an API key. ```bash curl -X GET "https://api.recoupable.dev/api/tasks" \ -H "Authorization: Bearer YOUR_PRIVY_JWT" ``` The API validates the token against Privy, extracts the user's email, and resolves it to the corresponding Recoup account. Bearer tokens always authenticate as a personal account — they cannot act on behalf of an organization. --- ## How We Verify Access on API Calls Every authenticated request goes through `validateAuthContext`, which enforces the following access rules: ### API Key or Bearer Token By default, requests access the key owner's own account. When `account_id` is provided: ``` Request includes account_id override? ├── Same as key owner → Allowed (self-access) ├── Key owner is a member of an org that contains account_id → Allowed └── No matching org membership → 403 Forbidden ``` Membership is verified by checking the key owner's [organizations](https://recoupable.dev/docs/api-reference/organizations/list) for a record linking the account to the target account's organization. > **Note** > The Recoup internal admin organization has universal access to all accounts. ### Organization Access via `organization_id` Some endpoints accept an `organization_id` parameter. When provided, the API additionally validates that the authenticated account is either: - A **member** of the organization, or - The **organization account itself** --- ## Error Responses | Status | Cause | | --- | --- | | `401` | Missing or invalid credentials, or both `x-api-key` and `Authorization` headers provided | | `403` | Valid credentials but insufficient access to the requested `account_id` or `organization_id` | --- ## Security Notes - API keys are **never stored in plaintext** — only an HMAC-SHA256 hash (keyed with your project secret) is persisted in the database - **Never include `account_id` in your API key creation request** — the account is always derived from your authenticated credentials - Rotate keys immediately if compromised via the [API Keys Management Page](https://chat.recoupable.dev/keys) ## MCP OAuth and REST credentials Remote MCP uses host-managed OAuth. Its tokens remain in the host and are not REST credentials to extract. Use a separate personal API key or application-provided Privy JWT (Bearer token) for REST-only operations. Never put a key in global instructions or use a disposable account to gain access to customer data. The authorized account and organization membership still control every request. See [MCP OAuth](https://recoupable.dev/docs/mcp-oauth). # Credits Source: https://recoupable.dev/docs/credits How Recoup credits work — what's billed, how to check your balance, and how to upgrade. Some Recoup endpoints are billed in **credits** — primarily endpoints that hit external data providers, run AI inference, or generate content. Check the specific endpoint contract for other charges and subscription gates. --- ## What's billed | Family | Billed? | Notes | | --- | --- | --- | | **Research** ([`/api/research/*`](https://recoupable.dev/docs/api-reference/research/web)) | Yes | Each successful call deducts credits. Costs vary by endpoint and parameters (e.g. [`enrich`](https://recoupable.dev/docs/api-reference/research/enrich) charges by processor tier; [`extract`](https://recoupable.dev/docs/api-reference/research/extract) charges by URL count). | | **Content generation** ([`/api/content/image`](https://recoupable.dev/docs/api-reference/content/generate-image), [`video`](https://recoupable.dev/docs/api-reference/content/generate-video), upscale, transcribe and analyze) | Yes | Model-specific generation and inference charges apply. Hosted pipelines charge their component operations; estimates are advisory. | | **AI Chat — streaming** ([`POST /api/chat`](https://recoupable.dev/docs/api-reference/chat/workflow)) | Yes | Variable cost based on model token usage, with a per-request minimum. | | **Music analysis** ([`POST /api/songs/analyze`](https://recoupable.dev/docs/api-reference/songs/analyze)) | Yes | $0.05 per model call plus $0.001166 per second of inference time; `full_report` is the sum of its 13 calls. | | **Social scrape** ([`/api/socials/{id}/scrape`](https://recoupable.dev/docs/api-reference/social/scrape), [`/api/artist/socials/scrape`](https://recoupable.dev/docs/api-reference/artist/socials-scrape)) | Yes | $0.05, plus $0.01 per post requested via `posts` — per social profile scraped. | | **Other operations** | Consult the endpoint contract | [Artist CRUD](https://recoupable.dev/docs/api-reference/artists/list), [sandboxes](https://recoupable.dev/docs/api-reference/sandboxes/list), [sessions](https://recoupable.dev/docs/api-reference/sessions/get), [scheduled tasks](https://recoupable.dev/docs/api-reference/tasks/get), [account/org management](https://recoupable.dev/docs/api-reference/accounts/id), [agent signup](https://recoupable.dev/docs/api-reference/agents/signup), [Spotify proxies](https://recoupable.dev/docs/api-reference/spotify/search), etc. Subscription gating may still apply. | Upstream generation or inference may incur cost even if a later composition or delivery step fails. Do not infer zero spend from the final pipeline status; inspect usage events. Credit gates reject before provider work when the balance is insufficient. --- ## What a credit is worth **Balances and prices are US dollars.** The ledger stores amounts as integer micro-dollars, the same 6-decimal unit as USDC: `1,000,000 = $1.00`, so it can carry sub-cent charges (a $0.002/s provider rate prices exactly). Every credit field the API returns is that integer; divide by 1,000,000 to display it as currency. Example: [`GET /api/accounts/{id}/credits`](https://recoupable.dev/docs/api-reference/accounts/credits-get). --- ## Check your balance ```bash curl -sS https://api.recoupable.dev/api/accounts/$ACCOUNT_ID/credits \ -H "x-api-key: $RECOUP_API_KEY" ``` Response shape: ```json { "account_id": "acc_…", "remaining_credits": 292450000, "total_credits": 300000000, "used_credits": 7550000, "is_pro": true, "plan": "pro", "task_limit": null, "min_cadence_minutes": 60, "timestamp": "2026-04-24T17:50:43.475" } ``` `plan`, `task_limit`, and `min_cadence_minutes` describe the [plan](https://recoupable.dev/docs/credits#subscription) the balance belongs to. `total_credits` is your plan-derived monthly allotment. `remaining_credits` can exceed `total_credits` after a top-up or an admin grant, and `used_credits` clamps to 0 in that case. Full schema at [Get Account Credits](https://recoupable.dev/docs/api-reference/accounts/credits-get). To see what consumed the balance, list the charges line by line with [`GET /api/accounts/{id}/usage`](https://recoupable.dev/docs/api-reference/accounts/usage-get): one item per deduction, newest first, each with the amount in micro-dollars and as a dollar string, plus the total for the period. API charges name the endpoint that billed them in `model_id` (for example `POST /api/artist/socials/scrape`), so a list of charges reads as a list of calls. --- ## Subscription For Platform, Advisory, Build + Partner, and Enterprise options, see [Recoup pricing](https://recoupable.dev/pricing). API and MCP usage is billed separately through credits. Your account's current credit allowance and scheduled-task limits are returned by [Get Account Credits](https://recoupable.dev/docs/api-reference/accounts/credits-get). Use `total_credits`, `task_limit`, and `min_cadence_minutes` from that response rather than inferring limits from a public plan name. The API may return account identifiers such as `free`, `starter`, or `pro`. These are documented API values; they are not the names of consulting engagements. The request and response schemas define the values accepted by each endpoint. ### Task limits [`POST /api/tasks`](https://recoupable.dev/docs/api-reference/tasks/create) and [`PATCH /api/tasks`](https://recoupable.dev/docs/api-reference/tasks/update) [check the plan](https://recoupable.dev/docs/api-reference/accounts/credits-get) before writing. Creating a task past `task_limit`, re-enabling a disabled task past it, or saving a cron whose consecutive runs are closer together than `min_cadence_minutes` returns **HTTP 402** with `error: "plan_limit"`: ```json { "status": "error", "error": "plan_limit", "limit": "task_count", "message": "Free includes 1 task. Starter includes 3, Pro is unlimited.", "plan": "free", "task_limit": 1, "min_cadence_minutes": 10080, "current_task_count": 1, "billingUrl": "https://app.recoupable.dev/plan" } ``` `limit` is `task_count` or `min_cadence`; `current_task_count` excludes the task being created or updated. Edits that only touch `title`, `prompt`, or `model` never hit the gate. ### Monthly refill The refill is a **floor, not an assignment.** It raises `remaining_credits` up to your plan's monthly total and never lowers it. A balance already above the plan total, whether from a [top-up](https://recoupable.dev/docs/credits#one-time-top-ups) or an admin grant, is left exactly as it is, so credits you bought or were granted are never taken away by the calendar. The refill is lazy rather than scheduled: it applies on the next read of [`GET /api/accounts/{id}/credits`](https://recoupable.dev/docs/api-reference/accounts/credits-get) once your credits row is more than a month old. The balance you read is always the refilled one. ### Manage your subscription Sign in to your [account billing page](https://app.recoupable.dev/plan) to review your subscription, price, and current terms. For programmatic checkout, use the accepted request values in [Create Subscription Session](https://recoupable.dev/docs/api-reference/subscriptions/sessions-create) and open its returned checkout URL. Review the price and billing terms before completing checkout. Advisory and custom-build engagements begin with a conversation about scope. Their website plan selection does not create an API subscription or complete a payment. ### Check your tier ```bash curl -sS https://api.recoupable.dev/api/accounts/$ACCOUNT_ID/subscription \ -H "x-api-key: $RECOUP_API_KEY" ``` Get `$ACCOUNT_ID` from [`GET /api/accounts/id`](https://recoupable.dev/docs/api-reference/accounts/id) if you don't already have it. Response includes `isPro` (boolean), `status`, `plan`, and `source` (whether the subscription comes from the account itself or an organization the account belongs to). Full schema at [Get Subscription](https://recoupable.dev/docs/api-reference/accounts/subscription-get). ### One-time top-ups You can purchase credits any time via [`POST /api/credits/sessions`](https://recoupable.dev/docs/api-reference/credits/sessions-create). The endpoint adapts to what's on the account: - **Card on file → silent auto-charge.** Recoup charges your saved Stripe card off-session and returns `paymentIntentId`, `creditsPurchased`, and `totalCents`. Credits land within seconds. - **No card, or Stripe declines the saved card → Stripe Checkout fallback.** The response contains a Checkout `url` you open in the browser. When Stripe specifically declined a saved card, the response also includes a `declineReason` (e.g. `insufficient_funds`, `expired_card`) so you can explain *why* before sending the customer to update billing. ```bash curl -sS -X POST https://api.recoupable.dev/api/credits/sessions \ -H "x-api-key: $RECOUP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"credits": 1000000, "successUrl": "https://chat.recoupable.dev/credits/success"}' ``` Full request/response schema at [Create Credits Top-Up Session](https://recoupable.dev/docs/api-reference/credits/sessions-create). ### Check which card will be charged Before triggering a silent off-session charge, inspect the default payment method on file: ```bash curl -sS https://api.recoupable.dev/api/accounts/$ACCOUNT_ID/payment-method \ -H "x-api-key: $RECOUP_API_KEY" ``` Response shape: ```json { "account_id": "acc_…", "card": { "brand": "visa", "last4": "4242", "exp_month": 12, "exp_year": 2026, "funding": "credit" } } ``` `card` is `null` when no payment method has been saved yet — the next top-up call will route through a checkout session to collect one. Expired cards are still returned (with their original `exp_month` / `exp_year`); callers should compare against the current date and warn the customer, since an off-session charge against an expired card will decline. Full schema at [Get Default Payment Method](https://recoupable.dev/docs/api-reference/accounts/payment-method-get). --- ## Running out of credits Every billed API request runs a credit gate before it executes. If `remaining_credits` doesn't cover the request's cost, the request stops there and returns **HTTP 402**. Nothing is charged and no Stripe object is created. The decision tree: 1. **Enough credits?** → Request proceeds, credits deducted on success. 2. **Short?** → Request returns **HTTP 402** with the balance, the cost, and a link to billing. A card on the account is never charged on its own. Charging happens only when the account asks to buy credits, through [`POST /api/credits/sessions`](https://recoupable.dev/docs/api-reference/credits/sessions-create), or when the account has turned on auto top-up. Saving a card ahead of time via [`POST /api/accounts/{id}/payment-method`](https://recoupable.dev/docs/api-reference/accounts/payment-method-create) makes a one-time purchase a single call instead of a browser round-trip; it does not authorize a charge by itself. Your plan, card, and every payment are on [`/api/accounts/{id}/subscription`](https://recoupable.dev/docs/api-reference/accounts/subscription-get), [`/api/accounts/{id}/payment-method`](https://recoupable.dev/docs/api-reference/accounts/payment-method-get), and [`/api/accounts/{id}/payments`](https://recoupable.dev/docs/api-reference/accounts/payments-get); pass an organization id as `{id}` to read the organization's billing. ### Auto top-up (opt-in) Auto top-up is off for every account until it is turned on with [`PUT /api/accounts/{id}/auto-top-up`](https://recoupable.dev/docs/api-reference/accounts/auto-top-up-update), which needs three things chosen by the account: `enabled`, the `amountCents` to buy each time (5.00 to 1,000.00 USD), and the `thresholdCents` balance that triggers it. Both are USD cents; the `remaining_credits` balance above is in credit micro-dollars, so divide it by 10,000 to compare. Turning it on requires a card on file. Once on, the first credit deduction that leaves the balance below the threshold charges the card for the amount, grants the credits, records a usage event, and emails a receipt. Guardrails: at most one top-up per account per 10 minutes; a card decline turns auto top-up off, records the decline message as `lastError` (returned by [`GET /api/accounts/{id}/auto-top-up`](https://recoupable.dev/docs/api-reference/accounts/auto-top-up-get)), and emails the account instead of retrying; removing the card turns it off. Auto top-up never creates a checkout session and never touches an invoiced (enterprise) plan. Saving a card does not turn auto top-up on by itself. --- ## 402 Payment Required When the gate comes up short, billed endpoints return **HTTP 402** with a unified body: ```json { "error": "insufficient_credits", "remaining_credits": 120000, "required_credits": 1000000, "billingUrl": "https://app.recoupable.dev" } ``` | Field | When it appears | Meaning | | --- | --- | --- | | `error` | Always | Literally `"insufficient_credits"` | | `remaining_credits` | Always | Account balance at the moment of the failed request | | `required_credits` | Always | What this endpoint needed | | `billingUrl` | Always | Static link to the Recoup app, where a human can save a card and buy credits | `billingUrl` is a constant, not a freshly minted Stripe Checkout Session. Retrying a credit-gated endpoint returns the same URL every time and creates nothing, so an unattended client that keeps hitting the gate is safe to leave running. **How to react:** - **Browser-driven UI** (e.g., the Recoup chat app): send the customer to `billingUrl`, or call [`POST /api/credits/sessions`](https://recoupable.dev/docs/api-reference/credits/sessions-create) inline and open the `url` it returns. - **Programmatic / LLM-driven client**: report `remaining_credits` and `required_credits`, and surface `billingUrl` as the link a human needs to visit. Do not treat a 402 as retryable; the balance will not change on its own until someone buys credits or the monthly refill lands. --- ## Cost per endpoint Current as of this revision of the page. The authoritative source is the per-endpoint reference docs and the `required_credits` field on any 402 response. | Endpoint | Cost | | --- | --- | | [`POST /api/chat`](https://recoupable.dev/docs/api-reference/chat/workflow) (streaming) | Variable, priced from the model's token usage; see `required_credits` on a 402 | | [Recoup artist and track research](https://recoupable.dev/docs/api-reference/research/search) | $0.05 per successful research call (50,000 credits); track-name resolution can add separately charged provider lookups | | [`POST /api/research/people`](https://recoupable.dev/docs/api-reference/research/people) | $0.05 per call | | [`POST /api/research/web`](https://recoupable.dev/docs/api-reference/research/web) | $0.01 per call | | [`POST /api/research/extract`](https://recoupable.dev/docs/api-reference/research/extract) | $0.05 × number of URLs | | [`POST /api/research/enrich`](https://recoupable.dev/docs/api-reference/research/enrich) | $0.05 / $0.10 / $0.25 (base / core / ultra processor) | | [`POST /api/research/deep`](https://recoupable.dev/docs/api-reference/research/deep) | $0.25 per call | | [`POST /api/image/generate`](https://recoupable.dev/docs/api-reference/image/generation) | Per-call price; see endpoint reference | | [`POST /api/songs/analyze`](https://recoupable.dev/docs/api-reference/songs/analyze) | `$0.05 + $0.001166 × elapsed_seconds` per model call; `full_report` = 13 calls | ## Billing recovery and units `insufficient_credits` returns `billingUrl`, a static app link, not a Stripe Checkout session. Stop and surface it; do not silently retry or initiate a charge. The separate historical measurement card-setup gate returns `checkoutUrl`; subscription Checkout returns `url`. Inspect the endpoint-specific error type. Top-ups require `successUrl` on every request and a positive whole-cent multiple of 10,000 micro-dollar credits. For example, 2,500,000 credits buys $2.50 before fees; `totalCents` is 289 for that example. It is not a credit count. `isPro` means effective Pro only, and is false for paid Starter; use `plan` for paid-plan decisions. Scheduled task creation and updates can return a plan-limit 402; task reads do not run that gate. Free/Starter/Pro are technical entitlement values. Platform/Advisory/Build/Enterprise marketing offers describe commercial services; they are not interchangeable names. # Research availability Source: https://recoupable.dev/docs/research-availability Explore Recoup research, choose the right identifiers, and handle incomplete data. Recoup Research lets you discover artists and tracks, read current metrics, explore audience and playlist activity, and capture recording measurements. Authenticate with your Recoup API key or supported bearer credential. Recoup manages the data connections; you do not need a separate data-service key. Coverage varies by artist, recording, platform and metric. An empty result means no matching data was returned; it does not prove an artist has no activity or audience. ## Choose an identifier Search by name, inspect the matches, and pass the selected result's `id` to the detail endpoints. Treat research IDs as opaque strings: do not parse them or substitute a Recoup account UUID. Artist-name shortcuts use the first match; select an explicit ID when names are ambiguous. For portable recording references, store the ISRC and available Spotify or Apple Music identifiers. Additional response fields and provenance labels may vary as Recoup's data connections change. Preserve returned provenance for traceability without using it to choose a data vendor. ## Current track statistics Supply exactly one of `isrc`, `spotify_track_id`, or `apple_music_track_id`, plus the required `source` (the platform, such as `spotify`). Recoup uses stored Spotify measurements when available for an ISRC, and otherwise requests current research data. Preserve `data_source` and capture timestamps when returned; missing provenance remains unknown. A displayed play count is not a verified royalty-bearing stream count. ## Research endpoints | Method | Path | Reference | | --- | --- | --- | | GET | `/api/research` | [Search](https://recoupable.dev/docs/api-reference/research/search) | | GET | `/api/research/albums` | [Albums](https://recoupable.dev/docs/api-reference/research/albums) | | GET | `/api/research/audience` | [Audience](https://recoupable.dev/docs/api-reference/research/audience) | | GET | `/api/research/career` | [Career activities](https://recoupable.dev/docs/api-reference/research/career) | | GET | `/api/research/insights` | [Insights activities](https://recoupable.dev/docs/api-reference/research/insights) | | GET | `/api/research/lookup` | [Spotify artist lookup](https://recoupable.dev/docs/api-reference/research/lookup) | | GET | `/api/research/metrics` | [Current artist metrics](https://recoupable.dev/docs/api-reference/research/metrics) | | GET | `/api/research/milestones` | [Milestones](https://recoupable.dev/docs/api-reference/research/milestones) | | GET | `/api/research/playlists` | [Artist playlists](https://recoupable.dev/docs/api-reference/research/playlists) | | GET | `/api/research/profile` | [Artist profile](https://recoupable.dev/docs/api-reference/research/profile) | | GET | `/api/research/similar` | [Related artists](https://recoupable.dev/docs/api-reference/research/similar) | | GET | `/api/research/track` | [Track details](https://recoupable.dev/docs/api-reference/research/track) | | GET | `/api/research/track/playlists` | [Track playlists](https://recoupable.dev/docs/api-reference/research/track-playlists) | | GET | `/api/research/tracks` | [Artist tracks](https://recoupable.dev/docs/api-reference/research/tracks) | | GET | `/api/research/urls` | [Platform URLs](https://recoupable.dev/docs/api-reference/research/urls) | ## Understand the results - Artist metrics are current snapshots. Save dated results to compare them over time. - Related artists are unweighted; the accepted similarity axes do not change the ranking. - Artist playlist `status=past` uses all-time scope and can include current placements. - Career, insights and milestones expose available activities. They do not calculate a career score, create an AI report or guarantee a complete timeline. - Album results can include track-shaped catalog entries. Do not assume every entry is a distinct album. - Pagination and track-playlist filter behavior depend on the available data connection. Check the returned entries before assuming a filter was applied. ## Historical measurement jobs `POST /api/research/measurement-jobs` supports `source: "historical"` for Spotify backfill. It requires a card on file and returns queue counts with `id: null`; enqueueing does not confirm capture completion. Recordings with an existing historical research measurement can be skipped even when their history is partial. Inspect the available dates before claiming complete coverage. Current capture jobs return a snapshot ID. ## Costs and unavailable data Successful research calls cost $0.05 (50,000 micro-dollar credits). Track-name resolution can add separately charged lookup calls; reuse a selected research ID when available. Historical enqueue itself does not deduct research credits; capture work has its own balance and budget checks. For `402 insufficient_credits`, show the returned `billingUrl`. Surface `429` and unavailable-data errors; use bounded retries for transient failures and stop when the request cannot be served. Recoup does not promise a `Retry-After` header on these routes. Never fill a missing metric with an invented number or zero. # Create a New Artist Source: https://recoupable.dev/docs/workflows/create-artist End-to-end workflow to create, research and enrich a new artist account in a single session. This is the canonical recipe used internally by Recoup's chat agent. Follow it step-by-step to bring a new artist account up to "researched + enriched" parity from a sandbox or any external agent. The chain is 8 sequential steps. Long deterministic chains executed from prose memory tend to drop steps — the agent reads the doc once, runs a couple of calls, and forgets the rest. To prevent that from a sandbox, **drive the work from a checklist file**: scaffold the artist's `RECOUP.md` with one checkbox per step before any API call, then tick each box and persist captured values back to the frontmatter as you go. The file becomes the workflow state, and a fresh turn can resume by reading it. ## Prerequisites - `$RECOUP_API_KEY` (`x-api-key`) or `$RECOUP_ACCESS_TOKEN` (Privy Bearer JWT) for `api.recoupable.dev` - `$RECOUP_ORG_ID` — the org the artist should belong to (recommended in sandboxes) - An artist name to create (e.g. `ARTIST_NAME="The Weeknd"`) Select one credential obtained through the approved real-account REST setup: ```bash if [ -n "$RECOUP_API_KEY" ]; then AUTH=(-H "x-api-key: $RECOUP_API_KEY") elif [ -n "$RECOUP_ACCESS_TOKEN" ]; then AUTH=(-H "Authorization: Bearer $RECOUP_ACCESS_TOKEN") else echo "Authenticate REST through the approved real-account setup flow." >&2 exit 1 fi ``` Use your real account and selected organization. Do not create a disposable `agent+` identity or paste credentials into the workspace file. The flow has three phases, run from a single checklist file: 1. **Create + identify** — `POST /api/artists`, then find the canonical Spotify match 2. **Enrich** — `PATCH` the artist with image/label/socials, then read available Recoup research and search the web for narrative context and additional socials 3. **Synthesize + persist** — generate a knowledge-base report, save it (RECOUP.md tree or hosted URL), then optionally `PATCH` the `knowledges` array ## Step 0: Scaffold the workspace BEFORE any API call Pick a slug, make the directory, and write the initial `RECOUP.md` template — frontmatter holds the values the chain captures (filled as you go); body holds the unchecked steps: ```bash ARTIST_SLUG=$(echo "$ARTIST_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]\+/-/g; s/^-//; s/-$//') ARTIST_DIR="artists/$ARTIST_SLUG" mkdir -p "$ARTIST_DIR" cat > "$ARTIST_DIR/RECOUP.md" <&2 return 1 ;; 429) retry_after=$(awk 'tolower($1)=="retry-after:" {sub(/^[^:]*:[[:space:]]*/, ""); sub(/\r$/, ""); print}' "$headers") rm -f "$headers" printf 'Research step stopped. Retry-After: %s. Resume manually when allowed.\n' "${retry_after:-not provided}" >&2 return 1 ;; 5??) ;; *) rm -f "$headers"; printf 'Research HTTP %s; resolve before resuming.\n' "$status" >&2; return 1 ;; esac fi if [ "$attempt" -lt 3 ]; then sleep "$((attempt * 2))"; fi done rm -f "$headers" echo 'Research unavailable after three attempts.' >&2 return 1 } ``` ### 4a: Resolve the research artist ID ```bash LOOKUP=$(research_request lookup --get \ --data-urlencode "spotifyId=$SPOTIFY_ARTIST_ID") || exit 1 RESEARCH_ARTIST_ID=$(printf '%s' "$LOOKUP" | jq -r \ '[.id, .artist_info.id] | map(select(type == "string") | select(test("\\S"))) | first // empty') ``` Save this value as `researchArtistId`. It is separate from the Recoup artist account UUID and Spotify ID. Do not coerce it to a number or copy an older saved research ID into it; re-resolve and verify older workspaces. If a successful lookup has no usable `id`, record that gap and skip 4b–4d, then run 4e. See [Artist Lookup](https://recoupable.dev/docs/api-reference/research/lookup) for the response contract. ### 4b–4d: Read available structured signals ```bash if [ -n "$RESEARCH_ARTIST_ID" ]; then PROFILE=$(research_request profile --get \ --data-urlencode "id=$RESEARCH_ARTIST_ID") || exit 1 CAREER=$(research_request career --get \ --data-urlencode "id=$RESEARCH_ARTIST_ID") || exit 1 PLAYLISTS=$(research_request playlists --get \ --data-urlencode "id=$RESEARCH_ARTIST_ID") || exit 1 fi ``` Inspect the fields actually returned. Profile data may be partial; `career` is an available activity feed, not a guaranteed complete career timeline. `playlists` returns `placements`, which may be empty. Do not turn missing data into zero or infer a label, achievement or placement that is absent. See [Artist Profile](https://recoupable.dev/docs/api-reference/research/profile), [Artist Activity](https://recoupable.dev/docs/api-reference/research/career) and [Playlist Placements](https://recoupable.dev/docs/api-reference/research/playlists). ### 4e: Search for narrative, press and collaborations ```bash RESEARCH_WEB=$(research_request web -X POST \ -H "Content-Type: application/json" \ -d "$(jq -n --arg name "$ARTIST_NAME" \ '{query: ($name + " biography press recent collaborations")}')") || exit 1 ``` Verify source links before asserting a fact. See [Web Search](https://recoupable.dev/docs/api-reference/research/web). **After this step:** persist `researchArtistId` when available. Save the returned payloads, source links and coverage gaps under `## Research` in `RECOUP.md`, with one subsection per request. Tick step 4 only after the successful reads and any intentional structured-data skip have been recorded. ## Step 5: Pull the Spotify catalog ```bash TOP_TRACKS=$(curl -sS -G "https://api.recoupable.dev/api/spotify/artist/topTracks" \ "${AUTH[@]}" \ --data-urlencode "id=$SPOTIFY_ARTIST_ID") ALBUMS=$(curl -sS -G "https://api.recoupable.dev/api/spotify/artist/albums" \ "${AUTH[@]}" \ --data-urlencode "id=$SPOTIFY_ARTIST_ID") # For each notable album, drill in (ALBUM_ID from $ALBUMS): ALBUM_DETAIL=$(curl -sS -G "https://api.recoupable.dev/api/spotify/album" \ "${AUTH[@]}" \ --data-urlencode "id=$ALBUM_ID") ``` `$SPOTIFY_ARTIST_ID` is the value you exported in step 2. See [Spotify Top Tracks](https://recoupable.dev/docs/api-reference/spotify/artist-top-tracks), [Spotify Artist Albums](https://recoupable.dev/docs/api-reference/spotify/artist-albums), and [Spotify Album](https://recoupable.dev/docs/api-reference/spotify/album) for full schemas. **After this step:** populate the artist's `releases/` folder — write one `releases/{release-slug}/RELEASE.md` per album (album slugs are bare, `-ep` / `-single` / `-compilation` suffixes for other types) using the per-album `GET /api/spotify/album?id=$ALBUM_ID` response, and write the top tracks snapshot to `releases/top-tracks.md`. `RELEASE.md` is the 18-section master release-management document (project snapshot, identifiers, narrative, audience, DSP strategy, marketing, social, PR, visuals, physical/merch/touring, team, budget, KPI tracking, links hub, plus Outstanding Deliverables and a Document History log) — Step 5 fills the Spotify-derivable fields and leaves the rest as `⚠️ TBD`. The full template + the field-by-field Spotify mapping live in the [`artist-workspace`](https://github.com/recoupable/recoup-platform-plugin/tree/main/skills/artist-workspace) skill at `references/release-template.md`. Tick `- [ ] 5.` → `- [x] 5.`. ## Step 6: Search the web for additional socials ```bash SOCIALS_SEARCH=$(curl -sS -X POST "https://api.recoupable.dev/api/research/web" \ "${AUTH[@]}" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg name "$ARTIST_NAME" \ '{query: ($name + " official instagram tiktok twitter youtube")}')") ``` Parse the results to extract any new social URLs whose host matches the platform reference table below. See [Web Search](https://recoupable.dev/docs/api-reference/research/web) for the request/response schema. **After this step:** save the discovered URLs (one per platform) into the `## Notes` section so step 7 can read them without re-querying. Tick `- [ ] 6.` → `- [x] 6.`. ## Step 7: PATCH the artist with the discovered socials ```bash curl -sS -X PATCH "https://api.recoupable.dev/api/artists/$ARTIST_ID" \ "${AUTH[@]}" \ -H "Content-Type: application/json" \ -d '{ "profileUrls": { "INSTAGRAM": "https://instagram.com/...", "TIKTOK": "https://tiktok.com/@...", "TWITTER": "https://x.com/...", "YOUTUBE": "https://youtube.com/@..." } }' ``` Only include keys for platforms you actually found URLs for. The PATCH preserves existing socials for keys you omit, so this is safe to run incrementally. See [Update Artist](https://recoupable.dev/docs/api-reference/artists/update) for the full body schema. **After this step:** tick `- [ ] 7.` → `- [x] 7.`. ## Step 8: Synthesize the knowledge base Combine the available structured and web research (`## Research` section), the Spotify catalog (`releases/`), and the discovered socials into a comprehensive markdown report. Recommended sections: - Artist biography and origin - Discography highlights (top tracks + key albums) - Spotify presence (genres, listener count, notable playlists) - Social media footprint - Recent activity / press - Notable collaborations and achievements Append the report to the same `RECOUP.md` you scaffolded in Step 0 — add it as a `## Knowledge base` section below the checklist. The path is: ``` artists/$ARTIST_SLUG/RECOUP.md ``` This dovetails with the [`artist-workspace`](https://github.com/recoupable/recoup-platform-plugin/tree/main/skills/artist-workspace) skill's filesystem conventions, so future sandbox sessions can read both the checklist and the KB without needing a hosted URL. **After this step:** tick `- [ ] 8.` → `- [x] 8.`. With every box ticked, the artist is fully set up. ## Platform key reference `profileUrls` keys are **uppercase** platform identifiers, inferred from the URL host. Recognized platforms: | URL contains | `profileUrls` key | | --- | --- | | `spotify.com` | `SPOTIFY` | | `instagram.com` | `INSTAGRAM` | | `tiktok.com` | `TIKTOK` | | `x.com`, `twitter.com` | `TWITTER` | | `youtube.` | `YOUTUBE` | | `apple.com` | `APPLE` | | `facebook.com` | `FACEBOOK` | | `threads.net`, `threads.com` | `THREADS` | URLs that don't match any of these are silently skipped — they won't be saved. ## What this workflow doesn't enforce There's no server-side orchestrator forcing each step to run in order — the chain is honor-system. The `RECOUP.md` checklist is what gives you determinism in practice: as long as you tick boxes and persist values after each step, a fresh turn (or a different agent) can pick up exactly where the last one stopped. If you skip a checkbox or skip the persist, the next turn won't know that step ran and may either redo it or, worse, treat downstream calls as ready when they aren't. A few constraints to honor: - **Run steps in order.** The frontmatter values written at one step are the inputs for later steps. - **Don't continue past a 4xx/5xx without recovery.** Leave the box unchecked, write the error to `## Notes`, and resolve before resuming. - **Treat the file as the source of truth.** If something isn't on disk, don't assume it ran. # Get Artists Source: https://recoupable.dev/docs/api-reference/artists/list Retrieve artists accessible to the authenticated account. The account is derived from the API key or Bearer token. When org_id is omitted, returns only the account's own artists. Pass org_id to view artists in a specific organization. Pass account_id to filter to a specific account the API key has access to. ## GET /api/artists Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/artists": { "get": { "description": "Retrieve artists accessible to the authenticated account. The account is derived from the API key or Bearer token. When org_id is omitted, returns only the account's own artists. Pass org_id to view artists in a specific organization. Pass account_id to filter to a specific account the API key has access to.", "parameters": [ { "name": "account_id", "in": "query", "description": "Filter to a specific account's artists. Only applicable when the authenticated account has access to multiple accounts via organization membership.", "required": false, "schema": { "type": "string", "format": "uuid" } }, { "name": "org_id", "in": "query", "description": "Filter to artists in a specific organization. When omitted, returns only personal (non-organization) artists.", "required": false, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Artists retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistsResponse" } } } }, "400": { "description": "Bad request - invalid parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistsErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid authentication" }, "403": { "description": "Forbidden - account_id is not accessible with the provided credentials" } } } } }, "components": { "schemas": { "ArtistsResponse": { "type": "object", "required": [ "status", "artists" ], "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "artists": { "type": "array", "items": { "$ref": "#/components/schemas/Artist" }, "description": "List of artist objects" }, "message": { "type": "string", "description": "Error message (only present if status is error)" } } }, "ArtistsErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } }, "Artist": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUID of the artist account. Equal to account_id; use this (or account_id) for /api/artists/{id}/* sub-resources." }, "account_id": { "type": "string", "format": "uuid", "description": "UUID of the artist account" }, "name": { "type": "string", "description": "Artist display name" }, "image": { "type": "string", "nullable": true, "description": "Artist profile image URL" }, "instruction": { "type": "string", "nullable": true, "description": "Artist-specific agent instruction / system prompt" }, "knowledges": { "type": "array", "description": "Artist knowledge-base entries", "items": { "type": "object", "properties": { "name": { "type": "string" }, "url": { "type": "string" }, "type": { "type": "string" } } } }, "label": { "type": "string", "nullable": true, "description": "Artist label / category" }, "pinned": { "type": "boolean", "description": "Whether the account has pinned this artist" }, "account_socials": { "type": "array", "items": { "$ref": "#/components/schemas/ArtistSocial" }, "description": "Social media profiles linked to the artist (embedded in the /api/artists response)" } } }, "ArtistSocial": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUID of the social profile" }, "platform": { "type": "string", "description": "Social media platform (e.g., instagram, twitter, tiktok)" }, "username": { "type": "string", "description": "Username on the platform" }, "profile_url": { "type": "string", "description": "Full URL to the social media profile" } } } } } } ``` # Get Artist Profile Source: https://recoupable.dev/docs/api-reference/artists/profile Public artist profile: the artist's name, image, connected social profiles, and linked catalogs in one call. ## GET /api/artists/{id}/profile Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/artists/{id}/profile": { "get": { "description": "Public artist profile: the artist's name, image, connected social profiles, and linked catalogs in one call.\n\n**No authentication.** This endpoint is deliberately public and unbilled — it backs the shareable artist page at `chat.recoupable.dev/artists/{id}` and can be called without an API key. Only public fields are returned; account settings, instructions, and valuation data are never included.\n\nResponses are cacheable (`Cache-Control: public, s-maxage=300, stale-while-revalidate=600`), so a value may be up to a few minutes stale.", "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier (UUID) of the artist account.", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Artist profile retrieved successfully.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistPublicProfileResponse" } } } }, "404": { "description": "No artist with this id. Returned for unknown ids and for accounts that are not artists — the two cases are deliberately indistinguishable, so the endpoint cannot be used to probe which account ids exist.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistPublicProfileErrorResponse" }, "example": { "status": "error", "error": "Artist not found" } } } } } } } }, "components": { "schemas": { "ArtistPublicProfileResponse": { "type": "object", "required": [ "id", "name", "image", "socials", "catalogs", "valuation" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "The artist's account id." }, "name": { "type": "string", "description": "The artist's display name." }, "image": { "type": "string", "nullable": true, "description": "URL of the artist's profile image, or null when none is set." }, "socials": { "type": "array", "description": "Connected social profiles, empty when none are linked.", "items": { "$ref": "#/components/schemas/ArtistPublicProfileSocial" } }, "catalogs": { "type": "array", "description": "Catalogs linked to the artist, newest first; empty when none are linked.", "items": { "$ref": "#/components/schemas/ArtistPublicProfileCatalog" } }, "valuation": { "$ref": "#/components/schemas/ArtistPublicProfileValuation", "description": "Estimated value band for the artist's catalog(s), from the published Recoup valuation model over all credited songs' latest play counts. Null when the artist has no measured songs.", "nullable": true } } }, "ArtistPublicProfileErrorResponse": { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "error": { "description": "Human-readable error message or documented error code.", "type": "string" } }, "required": [ "status", "error" ], "additionalProperties": {} }, "ArtistPublicProfileSocial": { "type": "object", "required": [ "type", "username", "profile_url" ], "properties": { "type": { "type": "string", "description": "Platform name derived from the profile URL, e.g. SPOTIFY, INSTAGRAM, TIKTOK, YOUTUBE.", "example": "SPOTIFY" }, "username": { "type": "string", "nullable": true, "description": "The artist's handle on the platform, when known." }, "profile_url": { "type": "string", "description": "Public URL of the profile on the platform." } } }, "ArtistPublicProfileCatalog": { "type": "object", "required": [ "id", "name", "song_count", "updated_at", "songs" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "The catalog id." }, "name": { "type": "string", "description": "The catalog's display name." }, "song_count": { "type": "integer", "description": "Number of songs currently in the catalog." }, "updated_at": { "type": "string", "format": "date-time", "description": "When the catalog last changed." }, "songs": { "type": "array", "description": "The catalog's songs crediting this artist, sorted by plays descending and capped at the top 50 — `song_count` reflects the full catalog and may exceed `songs.length`.", "items": { "$ref": "#/components/schemas/ArtistPublicProfileSong" } } } }, "ArtistPublicProfileValuation": { "type": "object", "required": [ "low", "mid", "high" ], "properties": { "low": { "type": "number", "description": "Low end of the estimated catalog value band, USD." }, "mid": { "type": "number", "description": "Mid estimate of the catalog value, USD." }, "high": { "type": "number", "description": "High end of the estimated catalog value band, USD." } } }, "ArtistPublicProfileSong": { "type": "object", "required": [ "isrc", "name", "album", "artwork_url", "plays", "est_value_usd" ], "properties": { "isrc": { "type": "string", "description": "The song's ISRC.", "example": "QZTPX2553097" }, "name": { "type": "string", "description": "The song's title." }, "album": { "type": "string", "nullable": true, "description": "The album or release the song belongs to, when known." }, "artwork_url": { "type": "string", "nullable": true, "description": "Apple Music artwork URL for the song's release, or null when artwork has not been resolved yet." }, "plays": { "type": "integer", "description": "The song's latest captured Spotify play count. Captures are periodic, so this can lag the live number." }, "est_value_usd": { "type": "number", "description": "Estimated value of the song in USD — the mid point of the published Recoup valuation model applied to this song's plays." } } } } } } ``` # Create Artist Source: https://recoupable.dev/docs/api-reference/artists/create Create a new artist account. When spotify_artist_id is provided and a canonical artist already exists for it, the existing artist is linked to the account instead of creating a duplicate. The artist can optionally be linked to an organization. ## POST /api/artists Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/artists": { "post": { "description": "Create a new artist account. When spotify_artist_id is provided and a canonical artist already exists for it, the existing artist is linked to the account instead of creating a duplicate. The artist can optionally be linked to an organization.", "requestBody": { "description": "Artist creation parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateArtistRequest" } } } }, "responses": { "200": { "description": "Existing canonical artist for the given spotify_artist_id linked to the account (no new artist created)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateArtistResponse" } } } }, "201": { "description": "Artist created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateArtistResponse" } } } }, "400": { "description": "Bad request - validation error or invalid JSON", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateArtistError" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "CreateArtistRequest": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string", "minLength": 1, "description": "The name of the artist to create" }, "spotify_artist_id": { "type": "string", "description": "Optional Spotify artist id. When provided, the endpoint resolves-or-creates the canonical artist for that id: if an artist already carries it, that artist is linked to the account and returned (200) instead of creating a duplicate; otherwise the artist is created with its Spotify profile attached (201)." }, "account_id": { "type": "string", "format": "uuid", "description": "UUID of the account to create the artist for. Only applicable when the authenticated account has access to multiple accounts via organization membership. If not provided, the artist is created for the API key's own account." }, "organization_id": { "type": "string", "format": "uuid", "description": "Optional organization ID to link the new artist to" } } }, "CreateArtistResponse": { "type": "object", "required": [ "artist" ], "properties": { "artist": { "$ref": "#/components/schemas/CreatedArtist" } } }, "CreateArtistError": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "missing_fields": { "type": "array", "items": { "type": "string" }, "description": "List of missing or invalid field names" }, "error": { "type": "string", "description": "Error message describing the validation failure" }, "message": { "type": "string", "description": "Error message (for invalid JSON or other errors)" } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } }, "CreatedArtist": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUID of the created artist account" }, "account_id": { "type": "string", "format": "uuid", "description": "UUID of the artist account (same as id)" }, "name": { "type": "string", "description": "Name of the artist" }, "created_at": { "type": "string", "format": "date-time", "nullable": true, "description": "ISO timestamp of when the artist was created" }, "updated_at": { "type": "string", "format": "date-time", "nullable": true, "description": "ISO timestamp of when the artist was last updated" }, "image": { "type": "string", "nullable": true, "description": "Artist profile image URL" }, "instruction": { "type": "string", "nullable": true, "description": "Custom AI instruction for this artist" }, "knowledges": { "type": "array", "items": { "type": "string" }, "nullable": true, "description": "Knowledge base references for this artist" }, "label": { "type": "string", "nullable": true, "description": "Record label name" }, "organization": { "type": "string", "nullable": true, "description": "Organization name" }, "company_name": { "type": "string", "nullable": true, "description": "Company name" }, "job_title": { "type": "string", "nullable": true, "description": "Job title" }, "role_type": { "type": "string", "nullable": true, "description": "Role type" }, "onboarding_status": { "type": "string", "nullable": true, "description": "Onboarding status" }, "onboarding_data": { "nullable": true, "description": "Onboarding data" }, "account_info": { "type": "array", "description": "Account info records" }, "account_socials": { "type": "array", "description": "Linked social media accounts" } } } } } } ``` # Update Artist Source: https://recoupable.dev/docs/api-reference/artists/update Update an artist. All body fields are optional, but at least one must be provided. Use this endpoint to set basic profile (name, image, label), AI instructions, knowledge base entries, social profile URLs (mapped by platform), and pinned state for the calling account. ## PATCH /api/artists/{id} Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/artists/{id}": { "patch": { "description": "Update an artist. All body fields are optional, but at least one must be provided. Use this endpoint to set basic profile (name, image, label), AI instructions, knowledge base entries, social profile URLs (mapped by platform), and pinned state for the calling account.", "parameters": [ { "name": "id", "in": "path", "description": "Artist account ID to update.", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "description": "Fields to update. At least one field must be provided.", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateArtistRequest" }, "examples": { "pin": { "summary": "Pin for the authenticated account", "value": { "pinned": true } }, "unpin": { "summary": "Unpin for the authenticated account", "value": { "pinned": false } } } } } }, "responses": { "200": { "description": "Artist updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateArtistResponse" } } } }, "400": { "description": "Bad request - validation error (e.g. invalid image URL, no fields provided)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateArtistErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid authentication" }, "403": { "description": "Forbidden - the authenticated account cannot update this artist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateArtistErrorResponse" } } } }, "404": { "description": "Artist not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateArtistErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateArtistErrorResponse" } } } } } } } }, "components": { "schemas": { "UpdateArtistRequest": { "type": "object", "description": "All fields are optional. The request must include at least one field.", "minProperties": 1, "properties": { "name": { "type": "string", "description": "Artist display name", "example": "The Weeknd" }, "image": { "type": "string", "description": "Artist profile image URL. Pass an empty string to clear the existing image; otherwise must be a valid URL.", "example": "https://i.scdn.co/image/abc123" }, "instruction": { "type": "string", "description": "Custom AI instruction shown to assistants when this artist is the active context", "example": "Always reference the artist's R&B catalog when discussing collaborations." }, "label": { "type": "string", "description": "Record label name", "example": "XO / Republic Records" }, "knowledges": { "type": "array", "description": "Knowledge base entries for this artist. Replaces the existing array when provided.", "items": { "$ref": "#/components/schemas/ArtistKnowledge" } }, "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, BANDSINTOWN. Keys are matched case-sensitively, so lowercase keys will create duplicate socials instead of replacing the existing entry.", "additionalProperties": { "type": "string" }, "example": { "SPOTIFY": "https://open.spotify.com/artist/1Xyo4u8uXC1ZmMpatF05PJ", "INSTAGRAM": "https://instagram.com/theweeknd", "TIKTOK": "https://tiktok.com/@theweeknd", "TWITTER": "https://x.com/theweeknd", "YOUTUBE": "https://youtube.com/@theweeknd", "APPLE": "https://music.apple.com/us/artist/the-weeknd/479756766", "FACEBOOK": "https://facebook.com/theweeknd", "THREADS": "https://threads.net/@theweeknd", "BANDSINTOWN": "https://www.bandsintown.com/a/1371750-the-weeknd" } }, "pinned": { "type": "boolean", "description": "Pin or unpin the artist for the authenticated account. Affects only the caller's pin state, not other accounts." } } }, "UpdateArtistResponse": { "type": "object", "required": [ "artist" ], "properties": { "artist": { "$ref": "#/components/schemas/UpdatedArtist" } } }, "UpdateArtistErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "missing_fields": { "type": "array", "items": { "type": "string" }, "description": "Path to the field that failed validation, when applicable" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "ArtistKnowledge": { "type": "object", "required": [ "name", "url", "type" ], "properties": { "name": { "type": "string", "description": "Display name for the knowledge entry", "example": "Artist Knowledge Base Report" }, "url": { "type": "string", "format": "uri", "description": "Public URL where the knowledge content is stored", "example": "https://arweave.net/abc123" }, "type": { "type": "string", "description": "MIME type of the knowledge content", "example": "text/plain" } } }, "UpdatedArtist": { "type": "object", "required": [ "account_id", "name", "account_socials", "pinned" ], "properties": { "account_id": { "type": "string", "format": "uuid", "description": "UUID of the artist account" }, "name": { "type": "string", "nullable": true, "description": "Artist display name" }, "image": { "type": "string", "nullable": true, "description": "Artist profile image URL" }, "instruction": { "type": "string", "nullable": true, "description": "Custom AI instruction for this artist" }, "label": { "type": "string", "nullable": true, "description": "Record label name" }, "knowledges": { "type": "array", "nullable": true, "description": "Knowledge base entries for this artist", "items": { "$ref": "#/components/schemas/ArtistKnowledge" } }, "account_socials": { "type": "array", "description": "Social profiles linked to the artist after the update", "items": { "$ref": "#/components/schemas/UpdatedArtistSocial" } }, "pinned": { "type": "boolean", "description": "Whether the authenticated account has pinned this artist" } } }, "UpdatedArtistSocial": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUID of the underlying social record" }, "username": { "type": "string", "nullable": true, "description": "Username on the platform" }, "profile_url": { "type": "string", "nullable": true, "description": "Full profile URL on the platform" }, "link": { "type": "string", "description": "Profile URL (mirror of profile_url for legacy clients)" }, "type": { "type": "string", "description": "Uppercase platform identifier inferred from the profile URL: SPOTIFY, INSTAGRAM, TIKTOK, TWITTER, YOUTUBE, APPLE, FACEBOOK, THREADS, or NONE if the URL did not match a known platform." } } } } } } ``` # Pin Artist Source: https://recoupable.dev/docs/api-reference/artists/pin Update an artist. All body fields are optional, but at least one must be provided. Use this endpoint to set basic profile (name, image, label), AI instructions, knowledge base entries, social profile URLs (mapped by platform), and pinned state for the calling account. ## PATCH /api/artists/{id} Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/artists/{id}": { "patch": { "description": "Update an artist. All body fields are optional, but at least one must be provided. Use this endpoint to set basic profile (name, image, label), AI instructions, knowledge base entries, social profile URLs (mapped by platform), and pinned state for the calling account.", "parameters": [ { "name": "id", "in": "path", "description": "Artist account ID to update.", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "description": "Fields to update. At least one field must be provided.", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateArtistRequest" }, "examples": { "pin": { "summary": "Pin for the authenticated account", "value": { "pinned": true } }, "unpin": { "summary": "Unpin for the authenticated account", "value": { "pinned": false } } } } } }, "responses": { "200": { "description": "Artist updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateArtistResponse" } } } }, "400": { "description": "Bad request - validation error (e.g. invalid image URL, no fields provided)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateArtistErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid authentication" }, "403": { "description": "Forbidden - the authenticated account cannot update this artist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateArtistErrorResponse" } } } }, "404": { "description": "Artist not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateArtistErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateArtistErrorResponse" } } } } } } } }, "components": { "schemas": { "UpdateArtistRequest": { "type": "object", "description": "All fields are optional. The request must include at least one field.", "minProperties": 1, "properties": { "name": { "type": "string", "description": "Artist display name", "example": "The Weeknd" }, "image": { "type": "string", "description": "Artist profile image URL. Pass an empty string to clear the existing image; otherwise must be a valid URL.", "example": "https://i.scdn.co/image/abc123" }, "instruction": { "type": "string", "description": "Custom AI instruction shown to assistants when this artist is the active context", "example": "Always reference the artist's R&B catalog when discussing collaborations." }, "label": { "type": "string", "description": "Record label name", "example": "XO / Republic Records" }, "knowledges": { "type": "array", "description": "Knowledge base entries for this artist. Replaces the existing array when provided.", "items": { "$ref": "#/components/schemas/ArtistKnowledge" } }, "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, BANDSINTOWN. Keys are matched case-sensitively, so lowercase keys will create duplicate socials instead of replacing the existing entry.", "additionalProperties": { "type": "string" }, "example": { "SPOTIFY": "https://open.spotify.com/artist/1Xyo4u8uXC1ZmMpatF05PJ", "INSTAGRAM": "https://instagram.com/theweeknd", "TIKTOK": "https://tiktok.com/@theweeknd", "TWITTER": "https://x.com/theweeknd", "YOUTUBE": "https://youtube.com/@theweeknd", "APPLE": "https://music.apple.com/us/artist/the-weeknd/479756766", "FACEBOOK": "https://facebook.com/theweeknd", "THREADS": "https://threads.net/@theweeknd", "BANDSINTOWN": "https://www.bandsintown.com/a/1371750-the-weeknd" } }, "pinned": { "type": "boolean", "description": "Pin or unpin the artist for the authenticated account. Affects only the caller's pin state, not other accounts." } } }, "UpdateArtistResponse": { "type": "object", "required": [ "artist" ], "properties": { "artist": { "$ref": "#/components/schemas/UpdatedArtist" } } }, "UpdateArtistErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "missing_fields": { "type": "array", "items": { "type": "string" }, "description": "Path to the field that failed validation, when applicable" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "ArtistKnowledge": { "type": "object", "required": [ "name", "url", "type" ], "properties": { "name": { "type": "string", "description": "Display name for the knowledge entry", "example": "Artist Knowledge Base Report" }, "url": { "type": "string", "format": "uri", "description": "Public URL where the knowledge content is stored", "example": "https://arweave.net/abc123" }, "type": { "type": "string", "description": "MIME type of the knowledge content", "example": "text/plain" } } }, "UpdatedArtist": { "type": "object", "required": [ "account_id", "name", "account_socials", "pinned" ], "properties": { "account_id": { "type": "string", "format": "uuid", "description": "UUID of the artist account" }, "name": { "type": "string", "nullable": true, "description": "Artist display name" }, "image": { "type": "string", "nullable": true, "description": "Artist profile image URL" }, "instruction": { "type": "string", "nullable": true, "description": "Custom AI instruction for this artist" }, "label": { "type": "string", "nullable": true, "description": "Record label name" }, "knowledges": { "type": "array", "nullable": true, "description": "Knowledge base entries for this artist", "items": { "$ref": "#/components/schemas/ArtistKnowledge" } }, "account_socials": { "type": "array", "description": "Social profiles linked to the artist after the update", "items": { "$ref": "#/components/schemas/UpdatedArtistSocial" } }, "pinned": { "type": "boolean", "description": "Whether the authenticated account has pinned this artist" } } }, "UpdatedArtistSocial": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUID of the underlying social record" }, "username": { "type": "string", "nullable": true, "description": "Username on the platform" }, "profile_url": { "type": "string", "nullable": true, "description": "Full profile URL on the platform" }, "link": { "type": "string", "description": "Profile URL (mirror of profile_url for legacy clients)" }, "type": { "type": "string", "description": "Uppercase platform identifier inferred from the profile URL: SPOTIFY, INSTAGRAM, TIKTOK, TWITTER, YOUTUBE, APPLE, FACEBOOK, THREADS, or NONE if the URL did not match a known platform." } } } } } } ``` # Unpin Artist Source: https://recoupable.dev/docs/api-reference/artists/unpin Update an artist. All body fields are optional, but at least one must be provided. Use this endpoint to set basic profile (name, image, label), AI instructions, knowledge base entries, social profile URLs (mapped by platform), and pinned state for the calling account. ## PATCH /api/artists/{id} Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/artists/{id}": { "patch": { "description": "Update an artist. All body fields are optional, but at least one must be provided. Use this endpoint to set basic profile (name, image, label), AI instructions, knowledge base entries, social profile URLs (mapped by platform), and pinned state for the calling account.", "parameters": [ { "name": "id", "in": "path", "description": "Artist account ID to update.", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "description": "Fields to update. At least one field must be provided.", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateArtistRequest" }, "examples": { "pin": { "summary": "Pin for the authenticated account", "value": { "pinned": true } }, "unpin": { "summary": "Unpin for the authenticated account", "value": { "pinned": false } } } } } }, "responses": { "200": { "description": "Artist updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateArtistResponse" } } } }, "400": { "description": "Bad request - validation error (e.g. invalid image URL, no fields provided)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateArtistErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid authentication" }, "403": { "description": "Forbidden - the authenticated account cannot update this artist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateArtistErrorResponse" } } } }, "404": { "description": "Artist not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateArtistErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateArtistErrorResponse" } } } } } } } }, "components": { "schemas": { "UpdateArtistRequest": { "type": "object", "description": "All fields are optional. The request must include at least one field.", "minProperties": 1, "properties": { "name": { "type": "string", "description": "Artist display name", "example": "The Weeknd" }, "image": { "type": "string", "description": "Artist profile image URL. Pass an empty string to clear the existing image; otherwise must be a valid URL.", "example": "https://i.scdn.co/image/abc123" }, "instruction": { "type": "string", "description": "Custom AI instruction shown to assistants when this artist is the active context", "example": "Always reference the artist's R&B catalog when discussing collaborations." }, "label": { "type": "string", "description": "Record label name", "example": "XO / Republic Records" }, "knowledges": { "type": "array", "description": "Knowledge base entries for this artist. Replaces the existing array when provided.", "items": { "$ref": "#/components/schemas/ArtistKnowledge" } }, "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, BANDSINTOWN. Keys are matched case-sensitively, so lowercase keys will create duplicate socials instead of replacing the existing entry.", "additionalProperties": { "type": "string" }, "example": { "SPOTIFY": "https://open.spotify.com/artist/1Xyo4u8uXC1ZmMpatF05PJ", "INSTAGRAM": "https://instagram.com/theweeknd", "TIKTOK": "https://tiktok.com/@theweeknd", "TWITTER": "https://x.com/theweeknd", "YOUTUBE": "https://youtube.com/@theweeknd", "APPLE": "https://music.apple.com/us/artist/the-weeknd/479756766", "FACEBOOK": "https://facebook.com/theweeknd", "THREADS": "https://threads.net/@theweeknd", "BANDSINTOWN": "https://www.bandsintown.com/a/1371750-the-weeknd" } }, "pinned": { "type": "boolean", "description": "Pin or unpin the artist for the authenticated account. Affects only the caller's pin state, not other accounts." } } }, "UpdateArtistResponse": { "type": "object", "required": [ "artist" ], "properties": { "artist": { "$ref": "#/components/schemas/UpdatedArtist" } } }, "UpdateArtistErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "missing_fields": { "type": "array", "items": { "type": "string" }, "description": "Path to the field that failed validation, when applicable" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "ArtistKnowledge": { "type": "object", "required": [ "name", "url", "type" ], "properties": { "name": { "type": "string", "description": "Display name for the knowledge entry", "example": "Artist Knowledge Base Report" }, "url": { "type": "string", "format": "uri", "description": "Public URL where the knowledge content is stored", "example": "https://arweave.net/abc123" }, "type": { "type": "string", "description": "MIME type of the knowledge content", "example": "text/plain" } } }, "UpdatedArtist": { "type": "object", "required": [ "account_id", "name", "account_socials", "pinned" ], "properties": { "account_id": { "type": "string", "format": "uuid", "description": "UUID of the artist account" }, "name": { "type": "string", "nullable": true, "description": "Artist display name" }, "image": { "type": "string", "nullable": true, "description": "Artist profile image URL" }, "instruction": { "type": "string", "nullable": true, "description": "Custom AI instruction for this artist" }, "label": { "type": "string", "nullable": true, "description": "Record label name" }, "knowledges": { "type": "array", "nullable": true, "description": "Knowledge base entries for this artist", "items": { "$ref": "#/components/schemas/ArtistKnowledge" } }, "account_socials": { "type": "array", "description": "Social profiles linked to the artist after the update", "items": { "$ref": "#/components/schemas/UpdatedArtistSocial" } }, "pinned": { "type": "boolean", "description": "Whether the authenticated account has pinned this artist" } } }, "UpdatedArtistSocial": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUID of the underlying social record" }, "username": { "type": "string", "nullable": true, "description": "Username on the platform" }, "profile_url": { "type": "string", "nullable": true, "description": "Full profile URL on the platform" }, "link": { "type": "string", "description": "Profile URL (mirror of profile_url for legacy clients)" }, "type": { "type": "string", "description": "Uppercase platform identifier inferred from the profile URL: SPOTIFY, INSTAGRAM, TIKTOK, TWITTER, YOUTUBE, APPLE, FACEBOOK, THREADS, or NONE if the URL did not match a known platform." } } } } } } ``` # Delete Artist Source: https://recoupable.dev/docs/api-reference/artists/delete Delete an artist accessible to the authenticated account. ## DELETE /api/artists/{id} Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/artists/{id}": { "delete": { "description": "Delete an artist accessible to the authenticated account. This removes the caller's direct artist link and, if no owner links remain, deletes the artist account itself.", "parameters": [ { "name": "id", "in": "path", "description": "Artist account ID to delete.", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "description": "Optional account context override.", "required": false, "content": { "application/json": { "schema": { "type": "object", "properties": { "account_id": { "type": "string", "format": "uuid", "description": "UUID of the account whose artist should be deleted. Only applicable when the authenticated account has access to multiple accounts via organization membership or Recoup admin access. If not provided, the deletion runs in the API key's own account context." } } } } } }, "responses": { "200": { "description": "Artist deleted successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteArtistResponse" } } } }, "400": { "description": "Bad request - account_id is not a valid UUID", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteArtistErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid authentication" }, "403": { "description": "Forbidden - the authenticated account cannot delete this artist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteArtistErrorResponse" } } } }, "404": { "description": "Artist not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteArtistErrorResponse" } } } } } } } }, "components": { "schemas": { "DeleteArtistResponse": { "type": "object", "required": [ "success", "artistId" ], "properties": { "success": { "type": "boolean", "description": "Whether the artist delete completed successfully" }, "artistId": { "type": "string", "format": "uuid", "description": "UUID of the artist account that was deleted or unlinked" } } }, "DeleteArtistErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } } } } } ``` # Get Artist Socials Source: https://recoupable.dev/docs/api-reference/artists/socials Retrieve all social media profiles associated with an artist, with the latest follower counts from the most recent scrape. Call this before the Social Posts or Social Scrape endpoints to obtain the social IDs. Pass `history` to also get each profile's follower snapshots, so week-over-week movement is a read instead of something the caller has to remember. ## GET /api/artists/{id}/socials Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/artists/{id}/socials": { "get": { "description": "Retrieve all social media profiles associated with an artist, with the latest follower counts from the most recent scrape. Call this before the Social Posts or Social Scrape endpoints to obtain the social IDs. Pass `history` to also get each profile's follower snapshots, so week-over-week movement is a read instead of something the caller has to remember.", "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier of the artist account to fetch social profiles for", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "page", "in": "query", "description": "The page number to retrieve (default: 1)", "required": false, "schema": { "type": "integer", "default": 1 } }, { "name": "limit", "in": "query", "description": "The number of records per page (default: 20, max: 100)", "required": false, "schema": { "type": "integer", "default": 20, "maximum": 100 } }, { "name": "history", "in": "query", "description": "Number of days of follower snapshots to include per profile as `history`, newest first. Snapshots are written by every scrape that reports a follower count, one point per social per day (the latest scrape that day wins), so a profile has one point after its first scrape and a second only after a scrape on a later day. Omitted: no `history` field on the profiles.", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 90 }, "example": 14 } ], "responses": { "200": { "description": "Social profiles retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistSocialsResponse" } } } }, "400": { "description": "Bad request - invalid `id`, `page`, `limit` or `history`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistSocialsErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistSocialsErrorResponse" } } } }, "403": { "description": "Forbidden - caller does not have access to this artist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistSocialsErrorResponse" } } } }, "404": { "description": "Artist not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistSocialsErrorResponse" } } } } } } } }, "components": { "schemas": { "ArtistSocialsResponse": { "type": "object", "required": [ "status", "socials", "pagination" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "socials": { "type": "array", "items": { "$ref": "#/components/schemas/SocialProfile" }, "description": "List of social media profiles associated with the artist" }, "pagination": { "$ref": "#/components/schemas/ArtistSocialsPagination", "description": "Pagination metadata for the response" } } }, "ArtistSocialsErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } }, "SocialProfile": { "type": "object", "properties": { "social_id": { "type": "string", "format": "uuid", "description": "UUID of the artist's socials account" }, "username": { "type": "string", "description": "Username on the platform" }, "profile_url": { "type": "string", "description": "Direct URL to the profile" }, "avatar": { "type": "string", "nullable": true, "description": "URL to the profile avatar image" }, "bio": { "type": "string", "nullable": true, "description": "Profile biography or description" }, "follower_count": { "type": "integer", "nullable": true, "description": "Number of followers on this platform" }, "following_count": { "type": "integer", "nullable": true, "description": "Number of accounts followed on this platform" }, "region": { "type": "string", "nullable": true, "description": "Geographic region of the profile" }, "updated_at": { "type": "string", "format": "date-time", "description": "ISO timestamp of when the profile was last updated" }, "history": { "type": "array", "description": "Follower snapshots for the last `history` days, newest first. Present only when the `history` query parameter is passed.", "items": { "$ref": "#/components/schemas/SocialSnapshot" } } } }, "ArtistSocialsPagination": { "type": "object", "properties": { "total_count": { "type": "integer", "description": "Total number of social profiles available" }, "page": { "type": "integer", "description": "Current page number" }, "limit": { "type": "integer", "description": "Number of social profiles per page" }, "total_pages": { "type": "integer", "description": "Total number of pages available" } } }, "SocialSnapshot": { "type": "object", "required": [ "captured_at", "follower_count" ], "properties": { "captured_at": { "type": "string", "format": "date-time", "description": "When the scrape that produced this point completed" }, "follower_count": { "type": "integer", "description": "Followers (subscribers on YouTube) at capture time" }, "following_count": { "type": "integer", "nullable": true, "description": "Accounts followed at capture time; null where the platform does not report it" }, "post_count": { "type": "integer", "nullable": true, "description": "Lifetime post count at capture time (Instagram, TikTok, YouTube, X); null where the platform does not report it" } } } } } } ``` # Delete Artist Social Source: https://recoupable.dev/docs/api-reference/artists/socials-delete Remove a social profile link from an artist to correct a wrongly auto-matched social. ## DELETE /api/artists/{id}/socials/{socialId} Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/artists/{id}/socials/{socialId}": { "delete": { "description": "Remove a social profile link from an artist. Use this to correct a wrongly auto-matched social. This unlinks the social from the artist account; it does not delete the underlying social record, which may still be linked to other accounts.", "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier of the artist account the social is linked to", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "socialId", "in": "path", "description": "The unique identifier (UUID) of the social profile to unlink from the artist", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Social profile unlinked successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteArtistSocialResponse" } } } }, "400": { "description": "Bad request - id or socialId is not a valid UUID", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteArtistErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid authentication" }, "403": { "description": "Forbidden - the authenticated account cannot access this artist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteArtistErrorResponse" } } } }, "404": { "description": "Artist not found or the social is not linked to this artist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteArtistErrorResponse" } } } } } } } }, "components": { "schemas": { "DeleteArtistSocialResponse": { "type": "object", "required": [ "success", "socialId" ], "properties": { "success": { "type": "boolean", "description": "Whether the social profile was unlinked successfully" }, "socialId": { "type": "string", "format": "uuid", "description": "UUID of the social profile that was unlinked from the artist" } } }, "DeleteArtistErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } } } } } ``` # Get Artist Fans Source: https://recoupable.dev/docs/api-reference/fans/get Retrieve the social profiles of an artist's fans: the accounts that commented on the artist's scraped posts (Instagram today). Every Instagram scrape is followed by one batched profile run over the new commenters, which fills `avatar`, `bio`, `follower_count` and `following_count` on each fan; `region` is null unless the platform reports one. Supports pagination for large fan lists. ## GET /api/artists/{id}/fans Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/artists/{id}/fans": { "get": { "description": "Retrieve the social profiles of an artist's fans: the accounts that commented on the artist's scraped posts (Instagram today). Every Instagram scrape is followed by one batched profile run over the new commenters, which fills `avatar`, `bio`, `follower_count` and `following_count` on each fan; `region` is null unless the platform reports one. Supports pagination for large fan lists.", "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier of the artist account to fetch fans for", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "page", "in": "query", "description": "The page number to retrieve (default: 1)", "required": false, "schema": { "type": "integer", "default": 1 } }, { "name": "limit", "in": "query", "description": "The number of records per page (default: 20, max: 100)", "required": false, "schema": { "type": "integer", "default": 20, "maximum": 100 } } ], "responses": { "200": { "description": "Artist fans retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistFansResponse" } } } }, "400": { "description": "Bad request - missing required parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistFansErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistFansErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistFansErrorResponse" } } } } } } } }, "components": { "schemas": { "ArtistFansResponse": { "type": "object", "required": [ "status", "fans", "pagination" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "fans": { "type": "array", "items": { "$ref": "#/components/schemas/ArtistFan" }, "description": "List of social profiles from fans across all platforms" }, "pagination": { "$ref": "#/components/schemas/ArtistFansPagination", "description": "Pagination metadata for the response" } } }, "ArtistFansErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "ArtistFan": { "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the fan's social profile" }, "username": { "type": "string", "description": "Username or handle on the platform" }, "avatar": { "type": "string", "description": "URL to the fan's avatar/profile image" }, "profile_url": { "type": "string", "description": "Full URL to the fan's profile on the platform" }, "region": { "type": "string", "description": "Geographic region or location of the fan" }, "bio": { "type": "string", "description": "Fan's biography or profile description" }, "follower_count": { "type": "integer", "description": "Number of followers the fan has" }, "following_count": { "type": "integer", "description": "Number of accounts the fan is following" }, "updated_at": { "type": "string", "format": "date-time", "description": "ISO timestamp of when the fan data was last updated" } } }, "ArtistFansPagination": { "type": "object", "properties": { "total_count": { "type": "integer", "description": "Total number of records available" }, "page": { "type": "integer", "description": "Current page number" }, "limit": { "type": "integer", "description": "Number of records per page" }, "total_pages": { "type": "integer", "description": "Total number of pages available" } } } } } } ``` # Stream Chat Source: https://recoupable.dev/docs/api-reference/chat/workflow Stream sandbox-driven chat (Vercel Workflow) ## POST /api/chat Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/chat": { "post": { "summary": "Stream sandbox-driven chat (Vercel Workflow)", "description": "Streams an agent loop running as a durable [Vercel Workflow](https://vercel.com/docs/workflow) against the session's sandbox. The agent uses sandbox-only tools (`bash`, `read`, `write`, `grep`, `glob`, `todo`, `task`, `ask_user_question`, `skill`, `fetch`) — no MCP or Composio. Requires a sandbox provisioned via [`POST /api/sandbox`](/docs/api-reference/sandbox/create).\n\n**Related endpoints**\n- [`POST /api/chat/runs`](/docs/api-reference/chat/runs) — the **headless** counterpart: it provisions its own session + sandbox and runs without a streaming client. Use that for tasks/cron/programmatic runs; use this for **interactive, streaming** turns on an existing chat.\n- [`GET /api/chat/{chatId}/stream`](/docs/api-reference/chat/workflow-stream) — reconnect to an in-progress response.\n- [`GET /api/chat/runs/{runId}`](/docs/api-reference/chat/runs-status) — check **whether** a headless run finished (status, not content).", "security": [ { "apiKeyAuth": [] } ], "requestBody": { "description": "Chat workflow request", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatWorkflowRequest" } } } }, "responses": { "200": { "description": "Server-Sent Events stream of UI message parts compatible with the Vercel AI SDK `createUIMessageStreamResponse`. The `x-workflow-run-id` response header carries the durable workflow run ID for resume/stop operations.", "headers": { "x-workflow-run-id": { "description": "Durable workflow run ID. Persist alongside the chat so a later `GET /api/chat/{chatId}/stream` request can resume this run after a disconnect.", "schema": { "type": "string" } } }, "content": { "text/event-stream": { "schema": { "type": "string", "description": "Server-Sent Events stream containing UI message parts" } } } }, "400": { "description": "Bad request — missing required fields, invalid body, or sandbox not initialized for the session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatStreamErrorResponse" } } } }, "401": { "description": "Missing or invalid API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatStreamErrorResponse" } } } }, "403": { "description": "The API key's account does not own the referenced `sessionId` or `chatId`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatStreamErrorResponse" } } } }, "404": { "description": "Session or chat not found.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatStreamErrorResponse" } } } }, "409": { "description": "A response is already streaming for this chat. Reconnect to it via `GET /api/chat/{chatId}/stream` instead of POSTing again.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatStreamErrorResponse" } } } } } } } }, "components": { "schemas": { "ChatWorkflowRequest": { "type": "object", "description": "Request body for the sandbox-driven, Workflow-backed chat endpoint. Requires an existing session, chat, and provisioned sandbox. The Bearer key from the `Authorization` header is forwarded into the sandbox for skill-based callbacks to the Recoup API — no separate access-token field needed.", "required": [ "messages", "chatId", "sessionId" ], "properties": { "messages": { "type": "array", "items": { "$ref": "#/components/schemas/UIMessage" }, "description": "Conversation history as an array of `UIMessage` objects, matching the [Vercel AI SDK](https://sdk.vercel.ai/) `UIMessage` shape. The last entry is typically the new user turn — the agent loop reads the entire array and persists the latest user message before the workflow starts." }, "chatId": { "type": "string", "format": "uuid", "description": "UUID of an existing chat under `sessionId`. The chat's `active_stream_id` column is used for compare-and-set race control across concurrent requests." }, "sessionId": { "type": "string", "format": "uuid", "description": "UUID of the session that owns both the chat and the sandbox. The session must have a non-null `sandbox_state` — call [POST /api/sandbox](/docs/api-reference/sandbox/create) first if not." }, "context": { "type": "object", "description": "Optional per-prompt context overrides forwarded into the agent loop.", "properties": { "contextLimit": { "type": "integer", "description": "Upper bound on the number of tokens / messages of conversation history the agent should consider for this prompt. Overrides the model's default for this call only." } } } } }, "ChatStreamErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ] }, "error": { "type": "string" }, "missing_fields": { "type": "array", "items": { "type": [ "string", "integer" ] }, "description": "Optional validation path for the first rejected field. String entries identify object/query fields; integer entries identify array positions. An empty path can represent validation of the path value itself. Lookup or workflow errors can omit this field." } } }, "UIMessage": { "type": "object", "required": [ "id", "role", "parts" ], "description": "AI SDK UIMessage. Parts are structurally validated before provisioning for headless runs. Legacy content-only messages are rejected.", "properties": { "id": { "type": "string" }, "role": { "type": "string", "enum": [ "user", "assistant", "system" ] }, "metadata": {}, "parts": { "type": "array", "minItems": 1, "items": { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string" } }, "additionalProperties": true }, "description": "SDK discriminated parts: text (text), file (mediaType and url), reasoning, supported tool and data-* parts. Each must satisfy the installed AI SDK schema; a type field alone is insufficient.", "example": [ { "type": "text", "text": "Summarize my next release." } ] } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." } } } } ``` # Resume Chat Stream Source: https://recoupable.dev/docs/api-reference/chat/workflow-stream Resume a chat response stream ## GET /api/chat/{chatId}/stream Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/chat/{chatId}/stream": { "get": { "summary": "Resume a chat response stream", "description": "Reconnects to an in-progress chat response — the resume counterpart to [`POST /api/chat`](/docs/api-reference/chat/workflow), which never resumes. Returns the live Server-Sent Events stream when a response is still being generated, or a `200` finish-only SSE stream when no active response exists or the run is terminal. The chat must belong to the authenticated account.\n\n**Related endpoints**\n- [`POST /api/chat`](/docs/api-reference/chat/workflow) — the interactive turn whose stream this resumes.\n- [`POST /api/chat/runs`](/docs/api-reference/chat/runs) — start a **headless** run, then pass the returned `chatId` here to **watch its output live**.\n- [`GET /api/chat/runs/{runId}`](/docs/api-reference/chat/runs-status) — check **whether** that run finished (status, not content).", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "chatId", "in": "path", "required": true, "description": "UUID of the chat to resume.", "schema": { "type": "string", "format": "uuid" } }, { "name": "account_id", "in": "query", "required": false, "description": "Act on a chat belonging to another account. Only honoured for a key that is authorised for that account — an organization key may pass a member account's id; a personal key passing anyone else's is rejected with `403 Access denied to specified account_id`. Omit it to act as the authenticated account.", "schema": { "type": "string", "format": "uuid" } }, { "name": "startIndex", "in": "query", "required": false, "description": "Zero-based index of the first stream chunk to return, so a client that was already receiving this response resumes where it left off instead of replaying the whole turn.\n\nPass the number of chunks already received. Omit it to receive the response from the beginning — the right choice for a fresh reader such as a page load, or a client watching a headless [`POST /api/chat/runs`](/docs/api-reference/chat/runs) run for the first time.\n\nA long turn's stream can end before the run does; reconnecting with `startIndex` is how a client keeps rendering without duplicating or skipping chunks.", "schema": { "type": "integer", "minimum": 0 } } ], "responses": { "200": { "description": "Server-Sent Events stream of UI message parts for the in-progress response, compatible with the Vercel AI SDK. The `x-workflow-run-id` response header carries the run ID for this response, and `x-workflow-stream-tail-index` carries the index of the last chunk known when this read was opened — a base for computing absolute positions, not a record of where the response ended. When there is nothing to resume, the stream contains one terminal finish chunk; workflow headers are absent on that path.", "headers": { "x-workflow-run-id": { "description": "Durable workflow run ID of the resumed run.", "schema": { "type": "string" } }, "x-workflow-stream-tail-index": { "description": "Zero-based index of the last chunk known to the stream **at the moment this read was opened**. Because headers are sent before the body, a read that stays open past that point will deliver chunks beyond it — so this is a base for computing absolute positions, not a record of where the response ended. A client resuming precisely should count the chunks it receives on top of this value. Omitted if the runtime cannot report it.", "schema": { "type": "integer" } } }, "content": { "text/event-stream": { "schema": { "type": "string", "description": "Server-Sent Events stream containing UI message parts" } } } }, "400": { "description": "Invalid chatId UUID or malformed startIndex. When supplied, startIndex must parse to a non-negative integer; blank, nonnumeric, fractional and negative values fail validation. Authentication/ownership is checked first. Validation errors include status: error, error and missing_fields when a validation path is available.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatStreamErrorResponse" } } } }, "401": { "description": "Missing or invalid authentication.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatStreamErrorResponse" } } } }, "403": { "description": "The caller does not own the chat session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatStreamErrorResponse" } } } }, "404": { "description": "Chat or owning session not found.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatStreamErrorResponse" } } } }, "500": { "description": "Session ownership lookup failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatStreamErrorResponse" } } } }, "502": { "description": "Workflow status lookup failed; this is not an empty or completed response.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatStreamErrorResponse" } } } } } } } }, "components": { "schemas": { "ChatStreamErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ] }, "error": { "type": "string" }, "missing_fields": { "type": "array", "items": { "type": [ "string", "integer" ] }, "description": "Optional validation path for the first rejected field. String entries identify object/query fields; integer entries identify array positions. An empty path can represent validation of the path value itself. Lookup or workflow errors can omit this field." } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Start Chat Run Source: https://recoupable.dev/docs/api-reference/chat/runs Start an asynchronous, headless chat-generation run on the durable agent workflow — the same engine as interactive [`POST /api/chat`](/docs/api-reference/chat/workflow). ## POST /api/chat/runs Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/chat/runs": { "post": { "description": "Start an asynchronous, headless chat-generation run on the durable agent workflow — the same engine as interactive [`POST /api/chat`](/docs/api-reference/chat/workflow).\n\n**Related endpoints**\n- [`POST /api/chat`](/docs/api-reference/chat/workflow) — use that for **interactive, streaming** turns on an existing chat; use this for **headless/programmatic** runs (no browser or pre-provisioned session needed).\n- [`GET /api/chat/runs/{runId}`](/docs/api-reference/chat/runs-status) — poll to learn **whether** the run finished (and if it succeeded).\n- [`GET /api/chat/{chatId}/stream`](/docs/api-reference/chat/workflow-stream) — **watch the output live** by passing the returned `chatId`.", "security": [ { "apiKeyAuth": [] } ], "requestBody": { "description": "Chat generation request", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatGenerateRequest" } } } }, "responses": { "202": { "description": "Run accepted. A durable workflow run was started; `runId` identifies it. `chatId` / `sessionId` identify the persisted output — read the result via [`GET /api/chat/{chatId}/stream`](/docs/api-reference/chat/workflow-stream) (resume the stream) or the chat's persisted messages. Poll [`GET /api/chat/runs/{runId}`](/docs/api-reference/chat/runs-status) for status.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatGenerateAcceptedResponse" } } }, "headers": { "Location": { "description": "Relative URL of the run-status resource for the started run.", "schema": { "type": "string", "format": "uri-reference" }, "example": "/api/chat/runs/wrun_01KVWZNM82NA7XKNEWWHG8VPHJ" } } }, "400": { "description": "Bad request - missing required parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatGenerateErrorResponse" } } } } } } } }, "components": { "schemas": { "ChatGenerateRequest": { "type": "object", "description": "Request body for chat generation. Exactly one of 'prompt' or 'messages' must be provided.", "properties": { "prompt": { "type": "string", "description": "Single text prompt for the assistant. Required if 'messages' is not provided.", "minLength": 1 }, "messages": { "type": "array", "items": { "$ref": "#/components/schemas/UIMessage" }, "description": "Array of UIMessage objects for context. Required if 'prompt' is not provided.", "minItems": 1 }, "artistId": { "type": "string", "format": "uuid", "description": "The unique identifier of the artist (optional)" }, "model": { "type": "string", "description": "The AI model to use for text generation (optional)", "example": "openai/gpt-5-mini" }, "trigger_run_id": { "type": "string", "description": "Optional. When the run is kicked off by a Trigger.dev scheduled task, the Trigger run id (`run_...`). The started workflow's `{ sessionId, chatId, workflowRunId }` are written to that Trigger run's metadata so the scheduled run can be traced to the chat that did the work.", "example": "run_06g3i0e3logru439uh9e1m8801" } }, "example": { "messages": [ { "id": "user-1", "role": "user", "parts": [ { "type": "text", "text": "Summarize my next release." } ] } ] } }, "ChatGenerateAcceptedResponse": { "type": "object", "required": [ "runId", "chatId", "sessionId" ], "description": "Confirmation that an asynchronous chat-generation run has been started on the durable agent workflow.", "properties": { "runId": { "type": "string", "description": "Durable workflow run id for the started generation. Same identifier surfaced as the `x-workflow-run-id` header on interactive [POST /api/chat](/docs/api-reference/chat/workflow).", "example": "wrun_01KVWZNM82NA7XKNEWWHG8VPHJ" }, "chatId": { "type": "string", "format": "uuid", "description": "Chat the run writes its assistant messages to. Use with [`GET /api/chat/{chatId}/stream`](/docs/api-reference/chat/workflow-stream) to resume the stream, or to fetch the persisted messages.", "example": "24830c6c-76d8-43be-ae22-1dfd545421ab" }, "sessionId": { "type": "string", "format": "uuid", "description": "Session (workspace + sandbox) provisioned for the run.", "example": "fa9c1516-35f0-4efd-a62c-01af26324e72" } } }, "ChatGenerateErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" }, "missing_fields": { "type": "array", "items": { "type": "string" }, "description": "Optional validation detail." } } }, "UIMessage": { "type": "object", "required": [ "id", "role", "parts" ], "description": "AI SDK UIMessage. Parts are structurally validated before provisioning for headless runs. Legacy content-only messages are rejected.", "properties": { "id": { "type": "string" }, "role": { "type": "string", "enum": [ "user", "assistant", "system" ] }, "metadata": {}, "parts": { "type": "array", "minItems": 1, "items": { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string" } }, "additionalProperties": true }, "description": "SDK discriminated parts: text (text), file (mediaType and url), reasoning, supported tool and data-* parts. Each must satisfy the installed AI SDK schema; a type field alone is insufficient.", "example": [ { "type": "text", "text": "Summarize my next release." } ] } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." } } } } ``` # Get Chat Run Status Source: https://recoupable.dev/docs/api-reference/chat/runs-status Status of an asynchronous run started via [`POST /api/chat/runs`](/docs/api-reference/chat/runs). Returns a point-in-time snapshot (**is it done?**) — not the generated content. ## GET /api/chat/runs/{runId} Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/chat/runs/{runId}": { "get": { "description": "Status of an asynchronous run started via [`POST /api/chat/runs`](/docs/api-reference/chat/runs). Returns a point-in-time snapshot (**is it done?**) — not the generated content.\n\n**Related endpoints**\n- [`POST /api/chat/runs`](/docs/api-reference/chat/runs) — starts the run this reports on.\n- [`GET /api/chat/{chatId}/stream`](/docs/api-reference/chat/workflow-stream) — read the **content**: poll *this* to know **whether** a run finished; use the stream to **watch the output** as it's produced.\n- [`POST /api/chat`](/docs/api-reference/chat/workflow) — the interactive, streaming counterpart to a headless run.", "security": [ { "apiKeyAuth": [] } ], "parameters": [ { "name": "runId", "in": "path", "required": true, "description": "The durable workflow run id returned by [POST /api/chat/runs](/docs/api-reference/chat/runs).", "schema": { "type": "string" }, "example": "wrun_01KVWZNM82NA7XKNEWWHG8VPHJ" } ], "responses": { "200": { "description": "Run status snapshot", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatRunStatusResponse" } } } }, "404": { "description": "Run not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatRunNotFoundError" } } } } } } } }, "components": { "schemas": { "ChatRunStatusResponse": { "type": "object", "required": [ "runId", "status" ], "description": "Point-in-time status of an asynchronous chat-generation run.", "properties": { "runId": { "type": "string", "description": "The durable workflow run id.", "example": "wrun_01KVWZNM82NA7XKNEWWHG8VPHJ" }, "status": { "type": "string", "enum": [ "queued", "running", "completed", "failed", "cancelled" ], "description": "Lifecycle state of the run. Read the produced content via the chat (`chatId` from the start response)." }, "createdAt": { "type": "string", "format": "date-time", "description": "When the workflow run was created.", "example": "2026-08-25T19:33:56.000Z", "nullable": true }, "startedAt": { "type": "string", "format": "date-time", "nullable": true, "description": "When the workflow run started executing; null while queued.", "example": "2026-08-25T19:34:16.386Z" }, "completedAt": { "type": "string", "format": "date-time", "nullable": true, "description": "When the workflow run reached a terminal state; null while queued or running.", "example": "2026-08-25T20:15:55.000Z" }, "durationMs": { "type": "integer", "nullable": true, "description": "`completedAt - startedAt` in milliseconds; null until the run is terminal.", "example": 2498614 } } }, "ChatRunNotFoundError": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ] }, "error": { "type": "string", "example": "Run not found" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." } } } } ``` # Stop Chat Source: https://recoupable.dev/docs/api-reference/chat/stop Stop the response currently being generated for a chat, freeing it to accept a new message. Idempotent — returns `stopped: false` when nothing is in progress. The chat must belong to the authenticated account. ## POST /api/chat/{chatId}/stop Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/chat/{chatId}/stop": { "post": { "description": "Stop the response currently being generated for a chat, freeing it to accept a new message. Idempotent — returns `stopped: false` when nothing is in progress. The chat must belong to the authenticated account.\n\nThe active slot is released only after cancellation is confirmed. Cancellation failure (502) or termination timeout (504) keeps it occupied; a later stop can retry. A successful no-active stop returns stopped: false.", "security": [ { "apiKeyAuth": [] } ], "parameters": [ { "name": "chatId", "in": "path", "required": true, "description": "UUID of the chat to stop.", "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Stop request processed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StopChatResponse" } } } }, "400": { "description": "chatId is not a valid UUID", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StopChatErrorResponse" } } } }, "401": { "description": "Missing or invalid authentication.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StopChatErrorResponse" } } } }, "403": { "description": "The chat belongs to a different account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StopChatErrorResponse" } } } }, "404": { "description": "Chat not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StopChatErrorResponse" } } } }, "500": { "description": "Session ownership lookup failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StopChatErrorResponse" } } } }, "502": { "description": "Workflow cancellation failed; the active slot remains held.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StopChatErrorResponse" } } } }, "504": { "description": "Workflow did not reach a terminal state before the wait deadline; the active slot remains held.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StopChatErrorResponse" } } } } } } } }, "components": { "schemas": { "StopChatResponse": { "type": "object", "required": [ "success", "stopped" ], "properties": { "success": { "type": "boolean", "description": "Always true when the request was processed.", "example": true }, "stopped": { "type": "boolean", "description": "True when an in-progress response was cancelled; false when nothing was running.", "example": true } } }, "StopChatErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." } } } } ``` # Create Chat Source: https://recoupable.dev/docs/api-reference/chat/create Create a new chat room. Optionally associate it with an artist and/or provide a client-generated chat ID. Pass accountId to create a chat on behalf of a specific account the API key has access to. ## POST /api/chats Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/chats": { "post": { "description": "Create a new chat room. Optionally associate it with an artist and/or provide a client-generated chat ID. Pass accountId to create a chat on behalf of a specific account the API key has access to.", "requestBody": { "description": "Chat creation parameters", "required": false, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateChatRequest" } } } }, "responses": { "200": { "description": "Chat created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateChatResponse" } } } }, "400": { "description": "Bad request - failed to create chat", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateChatErrorResponse" } } } }, "403": { "description": "Forbidden - API key does not have access to the specified accountId", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateChatErrorResponse" } } } } } } } }, "components": { "schemas": { "CreateChatRequest": { "type": "object", "properties": { "artistId": { "type": "string", "format": "uuid", "description": "UUID of the artist account the chat is associated with" }, "chatId": { "type": "string", "format": "uuid", "description": "UUID for the new chat (client-generated). If not provided, one will be generated automatically." }, "accountId": { "type": "string", "format": "uuid", "description": "UUID of the account to create the chat for. Only applicable when the authenticated account has access to multiple accounts via organization membership. If not provided, the chat is created for the API key's own account." }, "topic": { "type": "string", "description": "Topic name for the new chat room (e.g., 'Pulse Feb 2'). To edit the topic of an existing room, use [PATCH /api/chats](/docs/api-reference/chat/update)." } } }, "CreateChatResponse": { "type": "object", "required": [ "status", "chat" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request", "example": "success" }, "chat": { "$ref": "#/components/schemas/ChatRoom", "description": "The created chat room object" } } }, "CreateChatErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } }, "ChatRoom": { "type": "object", "required": [ "id", "title", "accountId", "sessionId", "updatedAt" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Chat id." }, "title": { "type": "string", "description": "Display title for the chat." }, "accountId": { "type": "string", "format": "uuid", "description": "Owning account for this chat." }, "sessionId": { "type": "string", "format": "uuid", "description": "Session that owns this chat. Combine with `id` to build the chat URL: `/sessions/{sessionId}/chats/{id}`." }, "updatedAt": { "type": "string", "format": "date-time", "description": "ISO timestamp of the last update." }, "artistId": { "type": "string", "format": "uuid", "nullable": true, "description": "Artist account id this chat's session was created in the context of, or `null` when no artist was associated. Inherited from `sessions.artist_id` on the owning session." } } } } } } ``` # Get Chats Source: https://recoupable.dev/docs/api-reference/chat/chats Returns chats for the authenticated account. Personal Bearer tokens see only the caller's chats; org keys see chats across the org's member accounts; admin keys see all chats. Each row carries the chat id and the session id needed to build the chat URL. ## GET /api/chats Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/chats": { "get": { "description": "Returns chats for the authenticated account. Personal Bearer tokens see only the caller's chats; org keys see chats across the org's member accounts; admin keys see all chats. Each row carries the chat id and the session id needed to build the chat URL.", "parameters": [ { "name": "artist_account_id", "in": "query", "description": "Optional. Filter chats to only include those whose owning session's `artistId` matches the supplied artist account id. Combine with `account_id` to scope by both owner and artist.", "required": false, "schema": { "type": "string", "format": "uuid" } }, { "name": "account_id", "in": "query", "description": "Filter to a specific account. Only applicable when the authenticated account has access to multiple accounts via organization membership.", "required": false, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Chats retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GetChatsResponse" } } } }, "400": { "description": "Bad request - invalid query parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GetChatsErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden - account_id is not a member of the organization or account tried to filter by an account_id they don't have access to", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "GetChatsResponse": { "type": "object", "required": [ "status", "chats" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request", "example": "success" }, "chats": { "type": "array", "items": { "$ref": "#/components/schemas/ChatRoom" }, "description": "Array of chat objects" } } }, "GetChatsErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } }, "ChatRoom": { "type": "object", "required": [ "id", "title", "accountId", "sessionId", "updatedAt" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Chat id." }, "title": { "type": "string", "description": "Display title for the chat." }, "accountId": { "type": "string", "format": "uuid", "description": "Owning account for this chat." }, "sessionId": { "type": "string", "format": "uuid", "description": "Session that owns this chat. Combine with `id` to build the chat URL: `/sessions/{sessionId}/chats/{id}`." }, "updatedAt": { "type": "string", "format": "date-time", "description": "ISO timestamp of the last update." }, "artistId": { "type": "string", "format": "uuid", "nullable": true, "description": "Artist account id this chat's session was created in the context of, or `null` when no artist was associated. Inherited from `sessions.artist_id` on the owning session." } } } } } } ``` # Get Chat Artist Source: https://recoupable.dev/docs/api-reference/chat/artist Retrieve the artist associated with a specific chat room. Returns 404 if the chat does not exist or is not accessible by the authenticated caller. ## GET /api/chats/{id}/artist Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/chats/{id}/artist": { "get": { "description": "Retrieve the artist associated with a specific chat room. Returns 404 if the chat does not exist or is not accessible by the authenticated caller.", "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier (UUID) of the chat room.", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Chat artist resolved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GetChatArtistResponse" } } } }, "400": { "description": "Bad request - invalid chat id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GetChatArtistErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found - chat does not exist or is not accessible", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GetChatArtistErrorResponse" } } } } } } } }, "components": { "schemas": { "GetChatArtistResponse": { "type": "object", "required": [ "status", "room_id", "artist_id", "artist_exists" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request", "example": "success" }, "room_id": { "type": "string", "format": "uuid", "description": "UUID of the chat room" }, "artist_id": { "type": "string", "format": "uuid", "nullable": true, "description": "UUID of the artist account associated with the chat, or null when no artist is linked" }, "artist_exists": { "type": "boolean", "description": "Whether an artist is linked to the chat room" } } }, "GetChatArtistErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } } } } } ``` # Update Chat Source: https://recoupable.dev/docs/api-reference/chat/update Update a chat room's topic (display name). The chatId is required; the topic field will be updated. Topic must be between 3 and 50 characters. ## PATCH /api/chats Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/chats": { "patch": { "description": "Update a chat room's topic (display name). The chatId is required; the topic field will be updated. Topic must be between 3 and 50 characters.", "requestBody": { "description": "Chat fields to update", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateChatRequest" } } } }, "responses": { "200": { "description": "Chat updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateChatResponse" } } } }, "400": { "description": "Bad request - invalid parameters or validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateChatErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden - API key does not have access to this chat", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found - chat room does not exist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateChatErrorResponse" } } } } } } } }, "components": { "schemas": { "UpdateChatRequest": { "type": "object", "required": [ "chatId", "topic" ], "properties": { "chatId": { "type": "string", "format": "uuid", "description": "The unique identifier (UUID) of the chat room to update" }, "topic": { "type": "string", "minLength": 3, "maxLength": 50, "description": "The new display name for the chat room. Must be between 3 and 50 characters." } } }, "UpdateChatResponse": { "type": "object", "required": [ "status", "chat" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request", "example": "success" }, "chat": { "$ref": "#/components/schemas/ChatRoom", "description": "The updated chat room object" } } }, "UpdateChatErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } }, "ChatRoom": { "type": "object", "required": [ "id", "title", "accountId", "sessionId", "updatedAt" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Chat id." }, "title": { "type": "string", "description": "Display title for the chat." }, "accountId": { "type": "string", "format": "uuid", "description": "Owning account for this chat." }, "sessionId": { "type": "string", "format": "uuid", "description": "Session that owns this chat. Combine with `id` to build the chat URL: `/sessions/{sessionId}/chats/{id}`." }, "updatedAt": { "type": "string", "format": "date-time", "description": "ISO timestamp of the last update." }, "artistId": { "type": "string", "format": "uuid", "nullable": true, "description": "Artist account id this chat's session was created in the context of, or `null` when no artist was associated. Inherited from `sessions.artist_id` on the owning session." } } } } } } ``` # Delete Chat Source: https://recoupable.dev/docs/api-reference/chat/delete Delete a chat room by ID. This operation also removes related room records (memory emails, memories) before deleting the room itself. ## DELETE /api/chats Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/chats": { "delete": { "description": "Delete a chat room by ID. This operation also removes related room records (memory emails, memories) before deleting the room itself.", "requestBody": { "description": "Chat deletion parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteChatRequest" } } } }, "responses": { "200": { "description": "Chat deleted successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteChatResponse" } } } }, "400": { "description": "Bad request - invalid parameters or validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteChatErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden - API key does not have access to this chat", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found - chat room does not exist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteChatErrorResponse" } } } }, "500": { "description": "Server error - failed to delete chat", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteChatErrorResponse" } } } } } } } }, "components": { "schemas": { "DeleteChatRequest": { "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "The unique identifier (UUID) of the chat room to delete." } } }, "DeleteChatResponse": { "type": "object", "required": [ "status", "id", "message" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request", "example": "success" }, "id": { "type": "string", "format": "uuid", "description": "The UUID of the deleted chat room." }, "message": { "type": "string", "description": "Success message describing the deletion result." } } }, "DeleteChatErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong." } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } } } } } ``` # Compact Chats Source: https://recoupable.dev/docs/api-reference/chat/compact Compact one or more chat conversations into summarized versions. This reduces the size of chat history while preserving key information. Optionally provide a prompt to control what information gets preserved in the compacted summary. ## POST /api/chats/compact Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/chats/compact": { "post": { "description": "Compact one or more chat conversations into summarized versions. This reduces the size of chat history while preserving key information. Optionally provide a prompt to control what information gets preserved in the compacted summary.", "requestBody": { "description": "Chat compaction parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CompactChatsRequest" } } } }, "responses": { "200": { "description": "Chats compacted successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CompactChatsResponse" } } } }, "400": { "description": "Bad request - invalid parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized - invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found - one or more chat IDs do not exist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "CompactChatsRequest": { "type": "object", "required": [ "chatId" ], "properties": { "chatId": { "type": "array", "minItems": 1, "items": { "type": "string", "format": "uuid" }, "description": "Array of chat IDs to compact" }, "prompt": { "type": "string", "description": "Optional prompt to control what information gets preserved in the compacted summary" } } }, "CompactChatsResponse": { "type": "object", "required": [ "chats" ], "properties": { "chats": { "type": "array", "items": { "$ref": "#/components/schemas/CompactedChat" }, "description": "Array of compacted chat results" } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } }, "CompactedChat": { "type": "object", "required": [ "chatId", "compacted" ], "properties": { "chatId": { "type": "string", "format": "uuid", "description": "The ID of the chat that was compacted" }, "compacted": { "type": "string", "description": "The compacted summary text of the chat" } } } } } } ``` # List Models Source: https://recoupable.dev/docs/api-reference/ai/models Returns the catalog of language models available through the Vercel AI Gateway. Embedding models are filtered out. The full schema mirrors the Vercel AI Gateway response (`GatewayLanguageModelEntry` from `@ai-sdk/gateway`); the documented properties below are the load-bearing fields used by clients. No authentication required. ## GET /api/ai/models Full OpenAPI specification: https://recoupable.dev/docs/spec/ai.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - AI", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://recoup-api.vercel.app" } ], "paths": { "/api/ai/models": { "get": { "description": "Returns the catalog of language models available through the Vercel AI Gateway. Embedding models are filtered out. The full schema mirrors the Vercel AI Gateway response (`GatewayLanguageModelEntry` from `@ai-sdk/gateway`); the documented properties below are the load-bearing fields used by clients. No authentication required.", "responses": { "200": { "description": "Model catalog retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AIModelsResponse" } } } }, "500": { "description": "Internal server error - failed to fetch models from the gateway", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AIModelsErrorResponse" } } } } } } } }, "components": { "schemas": { "AIModelsResponse": { "type": "object", "required": [ "models" ], "properties": { "models": { "type": "array", "items": { "$ref": "#/components/schemas/GatewayLanguageModelEntry" }, "description": "List of language models available through the Vercel AI Gateway" } } }, "AIModelsErrorResponse": { "type": "object", "required": [ "message" ], "properties": { "message": { "type": "string", "description": "Error message describing what went wrong" } } }, "GatewayLanguageModelEntry": { "type": "object", "additionalProperties": true, "description": "A language model entry as returned by the Vercel AI Gateway. Full schema mirrors the upstream `GatewayLanguageModelEntry` type from `@ai-sdk/gateway`.", "properties": { "id": { "type": "string", "description": "Fully qualified model identifier (e.g., 'anthropic/claude-3-5-sonnet')", "example": "anthropic/claude-3-5-sonnet" }, "name": { "type": "string", "description": "Human-readable display name for the model", "example": "Claude 3.5 Sonnet" }, "description": { "type": "string", "description": "Provider-supplied description of the model" }, "modelType": { "type": "string", "description": "Type of model - embedding models are filtered out before the response is returned", "example": "language" }, "pricing": { "type": "object", "additionalProperties": true, "description": "Per-token pricing information as reported by the gateway", "properties": { "input": { "type": "string", "description": "Cost per input token (USD)" }, "output": { "type": "string", "description": "Cost per output token (USD)" } } }, "specification": { "type": "object", "additionalProperties": true, "description": "Model specification metadata (provider, version, capabilities). Shape mirrors the gateway response." }, "context_window": { "type": "integer", "description": "Maximum number of tokens the model can process in a single request (input + output combined). Clients use this to gate model selection against per-request token budgets.", "example": 200000 }, "cost": { "type": "object", "description": "Per-million-token pricing in USD. Derived from `pricing` (which is per-token) for client-side display.", "required": [ "input", "output" ], "properties": { "input": { "type": "number", "description": "USD cost per million input tokens.", "example": 3 }, "output": { "type": "number", "description": "USD cost per million output tokens.", "example": 15 } } } } } } } } ``` # Get Chat Messages Source: https://recoupable.dev/docs/api-reference/chat/messages Retrieve all messages (memories) for a specific chat room in chronological order. ## GET /api/chats/{id}/messages Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/chats/{id}/messages": { "get": { "description": "Retrieve all messages (memories) for a specific chat room in chronological order.", "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier (UUID) of the chat room.", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Messages retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GetChatMessagesResponse" } } } }, "400": { "description": "Bad request - invalid chat id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GetChatMessagesErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden - caller does not have access to this chat", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found - chat does not exist or is not accessible", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GetChatMessagesErrorResponse" } } } }, "500": { "description": "Server error - failed to retrieve messages", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GetChatMessagesErrorResponse" } } } } } } } }, "components": { "schemas": { "GetChatMessagesResponse": { "type": "object", "required": [ "data" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/ChatMessage" }, "description": "Chronologically ordered list of messages for the chat" } } }, "GetChatMessagesErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } }, "ChatMessage": { "type": "object", "required": [ "id", "room_id", "content", "updated_at" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "UUID of the memory message" }, "room_id": { "type": "string", "format": "uuid", "description": "UUID of the parent chat room" }, "content": { "type": "object", "description": "Structured message payload stored for the memory" }, "updated_at": { "type": "string", "format": "date-time", "description": "ISO timestamp of the memory update" } } } } } } ``` # Copy Chat Messages Source: https://recoupable.dev/docs/api-reference/chat/messages-copy Copy all messages from the source chat (`id` path param) to a target chat (`targetChatId` in request body). By default, existing target messages are cleared before copy. ## POST /api/chats/{id}/messages/copy Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/chats/{id}/messages/copy": { "post": { "description": "Copy all messages from the source chat (`id` path param) to a target chat (`targetChatId` in request body). By default, existing target messages are cleared before copy.", "parameters": [ { "name": "id", "in": "path", "description": "The source chat room UUID.", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "description": "Copy target options", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CopyChatMessagesRequest" } } } }, "responses": { "200": { "description": "Messages copied successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CopyChatMessagesResponse" } } } }, "400": { "description": "Bad request - invalid payload or chat identifiers", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CopyChatMessagesErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden - caller lacks access to source or target chat", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CopyChatMessagesErrorResponse" } } } }, "404": { "description": "Not found - source or target chat does not exist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CopyChatMessagesErrorResponse" } } } }, "500": { "description": "Server error - failed to copy messages", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CopyChatMessagesErrorResponse" } } } } } } } }, "components": { "schemas": { "CopyChatMessagesRequest": { "type": "object", "required": [ "targetChatId" ], "properties": { "targetChatId": { "type": "string", "format": "uuid", "description": "Target chat room UUID to receive the copied messages." }, "clearExisting": { "type": "boolean", "default": true, "description": "When true, existing messages in the target chat are deleted before copy." } } }, "CopyChatMessagesResponse": { "type": "object", "required": [ "status", "source_chat_id", "target_chat_id", "copied_count", "cleared_existing" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request", "example": "success" }, "source_chat_id": { "type": "string", "format": "uuid", "description": "Source chat room UUID." }, "target_chat_id": { "type": "string", "format": "uuid", "description": "Target chat room UUID." }, "copied_count": { "type": "integer", "description": "Number of messages copied from source to target." }, "cleared_existing": { "type": "boolean", "description": "Whether existing target messages were deleted before copy." } } }, "CopyChatMessagesErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong." } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } } } } } ``` # Delete Trailing Chat Messages Source: https://recoupable.dev/docs/api-reference/chat/messages-trailing-delete Delete all messages in a chat from a given message onward (inclusive), identified by `from_message_id`. ## DELETE /api/chats/{id}/messages/trailing Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/chats/{id}/messages/trailing": { "delete": { "description": "Delete all messages in a chat from a given message onward (inclusive), identified by `from_message_id`.", "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier (UUID) of the chat room.", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "from_message_id", "in": "query", "description": "The message UUID from which trailing messages should be deleted (inclusive).", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Trailing messages deleted successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteTrailingChatMessagesResponse" } } } }, "400": { "description": "Bad request - invalid or missing query/path parameters or message does not belong to chat", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteTrailingChatMessagesErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteTrailingChatMessagesErrorResponse" } } } }, "403": { "description": "Forbidden - access denied to this chat", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteTrailingChatMessagesErrorResponse" } } } }, "404": { "description": "Not found - chat or message does not exist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteTrailingChatMessagesErrorResponse" } } } }, "500": { "description": "Server error - failed to delete trailing messages", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteTrailingChatMessagesErrorResponse" } } } } } } } }, "components": { "schemas": { "DeleteTrailingChatMessagesResponse": { "type": "object", "required": [ "status", "chat_id", "from_message_id" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request", "example": "success" }, "chat_id": { "type": "string", "format": "uuid", "description": "The chat UUID where deletion was applied." }, "from_message_id": { "type": "string", "format": "uuid", "description": "The message UUID used as the trailing deletion boundary." } } }, "DeleteTrailingChatMessagesErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong." }, "missing_fields": { "type": "array", "items": { "type": "string" }, "description": "List of missing or invalid parameter fields (when validation fails)." } } } } } } ``` # Research Search Source: https://recoupable.dev/docs/api-reference/research/search Search Recoup Research for artists, tracks or labels by name. Artist and track matches include an `id` when an identifier is available. Select a match and reuse that ID for detail requests. `type` accepts artists, tracks or labels, plus their singular aliases, case-insensitively. Playlist search is unavailable. Recoup defaults `limit` to 10; pagination strings are passed to the data connection and its limits apply. ## GET /api/research Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research": { "get": { "description": "Search Recoup Research for artists, tracks or labels by name. Artist and track matches include an `id` when an identifier is available. Select a match and reuse that ID for detail requests. `type` accepts artists, tracks or labels, plus their singular aliases, case-insensitively. Playlist search is unavailable. Recoup defaults `limit` to 10; pagination strings are passed to the data connection and its limits apply.\n\nUse your Recoup API key (`x-api-key`) or supported bearer credential. Successful artist and track research reads cost $0.05 (50,000 micro-dollar credits). Recoup manages the data connections. Coverage and rate limits vary; see [Research availability](/docs/research-availability).", "parameters": [ { "name": "q", "in": "query", "required": true, "description": "Required search text, such as an artist or track name. For a Spotify artist URL use the lookup endpoint.", "schema": { "type": "string" } }, { "name": "type", "in": "query", "required": false, "description": "Entity type to search: artists, tracks, or labels.", "schema": { "type": "string", "enum": [ "artists", "tracks", "labels", "artist", "track", "label" ], "default": "artists" } }, { "name": "limit", "in": "query", "required": false, "description": "Raw pagination string forwarded to the configured research data connection. Recoup defaults limit to 10; offset is omitted when absent. Provider limits apply.", "schema": { "type": "string", "default": "10" } }, { "name": "offset", "in": "query", "required": false, "description": "Raw pagination string forwarded to the configured research data connection. Recoup defaults limit to 10; offset is omitted when absent. Provider limits apply.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Search results", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchSearchResponse" } } } }, "400": { "description": "Validation error (e.g., missing required query parameter `q`)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Insufficient research credits. The response includes a static billingUrl; no checkout session is created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" } } } }, "403": { "description": "Credential, account or provider permission denied.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "404": { "description": "Artist/track resolution or requested provider data was not found. A failed name search may be reported as 404; retry with an exact provider ID when known.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "429": { "description": "Provider rate or quota limit. The adapter returns an error status but does not forward provider Retry-After headers. Avoid an unbounded retry loop.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "500": { "description": "Missing provider configuration, transport failure or internal error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" }, "502": { "description": "Upstream provider failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "504": { "description": "the configured research data connection request timed out.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } } } } } }, "components": { "schemas": { "ResearchSearchResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "example": "success" }, "results": { "type": "array", "description": "Matching artists, tracks or labels with provider identifiers and metadata.", "items": { "$ref": "#/components/schemas/ResearchSearchResult" } } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] }, "ResearchSearchResult": { "type": "object", "description": "Search result. Additional type-specific identifier fields pass through. The set depends on `type` (artists | tracks | labels).", "properties": { "id": { "type": "string", "description": "Provider-neutral ID." }, "name": { "type": "string" }, "avatar": { "type": "string", "nullable": true }, "site_url": { "type": "string", "nullable": true } }, "additionalProperties": true } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # Lookup by URL Source: https://recoupable.dev/docs/api-reference/research/lookup Resolve a Spotify artist URL or ID through Recoup Research. Supply `spotifyId` (22 alphanumeric characters) or a Spotify artist `url`; a nonempty `spotifyId` takes precedence. Apple Music URLs and Recoup account UUIDs are not accepted. Available artist metadata is returned; an `id` is included when the artist identifier can be resolved. ## GET /api/research/lookup Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/lookup": { "get": { "description": "Resolve a Spotify artist URL or ID through Recoup Research. Supply `spotifyId` (22 alphanumeric characters) or a Spotify artist `url`; a nonempty `spotifyId` takes precedence. Apple Music URLs and Recoup account UUIDs are not accepted. Available artist metadata is returned; an `id` is included when the artist identifier can be resolved.\n\nUse your Recoup API key (`x-api-key`) or supported bearer credential. Successful artist and track research reads cost $0.05 (50,000 micro-dollar credits). Recoup manages the data connections. Coverage and rate limits vary; see [Research availability](/docs/research-availability).", "parameters": [ { "name": "url", "in": "query", "required": false, "description": "Spotify artist URL. Required unless `spotifyId` is provided.", "schema": { "type": "string" } }, { "name": "spotifyId", "in": "query", "required": false, "description": "Spotify artist ID. Required unless `url` is provided.", "schema": { "type": "string", "example": "3TVXtAsR1Inumwj472S9r4", "pattern": "^[A-Za-z0-9]{22}$" } } ], "responses": { "200": { "description": "Artist profile matching the platform URL", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchLookupResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Insufficient research credits. The response includes a static billingUrl; no checkout session is created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" } } } }, "403": { "description": "Credential, account or provider permission denied.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "404": { "description": "Artist/track resolution or requested provider data was not found. A failed name search may be reported as 404; retry with an exact provider ID when known.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "429": { "description": "Provider rate or quota limit. The adapter returns an error status but does not forward provider Retry-After headers. Avoid an unbounded retry loop.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "500": { "description": "Missing provider configuration, transport failure or internal error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" }, "502": { "description": "Upstream provider failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "504": { "description": "the configured research data connection request timed out.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } } } } } }, "components": { "schemas": { "ResearchLookupResponse": { "type": "object", "description": "Artist resolved from a Spotify URL or ID.", "properties": { "status": { "type": "string", "example": "success" }, "result": { "type": "string", "example": "success" }, "message": { "type": "string", "example": "Data Retrieved." }, "artist_info": { "$ref": "#/components/schemas/ResearchArtistInfo" }, "id": { "type": "string", "description": "Opaque Recoup research artist ID when resolved. Reuse it in artist research requests; it is separate from the Recoup artist account UUID and Spotify ID." } }, "additionalProperties": true }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] }, "ResearchArtistInfo": { "type": "object", "description": "Full artist record — identity, bio, genres, platform links, and related artists.", "properties": { "name": { "type": "string" }, "avatar": { "type": "string" }, "site_url": { "type": "string" }, "country": { "type": "string", "description": "ISO country code.", "nullable": true }, "bio": { "type": "string", "nullable": true }, "genres": { "type": "array", "items": { "type": "string" } }, "links": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchLink" } }, "related_artists": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchArtistRef" } } }, "additionalProperties": true }, "ResearchLink": { "type": "object", "description": "External platform link.", "properties": { "source": { "type": "string", "description": "Platform name, e.g. `spotify`, `apple_music`." }, "external_id": { "type": "string", "description": "Platform-specific ID." }, "url": { "type": "string" }, "isrc": { "type": "string", "description": "ISRC (tracks only)." } }, "additionalProperties": true }, "ResearchArtistRef": { "type": "object", "description": "Provider-neutral artist reference.", "properties": { "id": { "type": "string", "description": "Provider-neutral artist ID.", "example": "wjcgfd9i" }, "name": { "type": "string", "example": "Drake" }, "avatar": { "type": "string", "description": "Artist image URL.", "nullable": true }, "site_url": { "type": "string", "description": "Artist page URL.", "nullable": true } }, "additionalProperties": true } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # Artist Profile Source: https://recoupable.dev/docs/api-reference/research/profile Read artist identity and available biography, links, genres and related-artist metadata. Fields depend on available research data. For current platform counts, use [Metrics](/docs/api-reference/research/metrics). ## GET /api/research/profile Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/profile": { "get": { "description": "Read artist identity and available biography, links, genres and related-artist metadata. Fields depend on available research data. For current platform counts, use [Metrics](/docs/api-reference/research/metrics).\n\nUse your Recoup API key (`x-api-key`) or supported bearer credential. Successful artist and track research reads cost $0.05 (50,000 micro-dollar credits). Recoup manages the data connections. Coverage and rate limits vary; see [Research availability](/docs/research-availability).", "parameters": [ { "name": "artist", "in": "query", "required": false, "description": "Artist name; Recoup resolves the first provider search match. Required unless id is supplied. Recoup artist UUID resolution is not implemented here.", "schema": { "type": "string" } }, { "name": "id", "in": "query", "required": false, "description": "Research artist ID returned by search or lookup; treat it as an opaque string. Required unless artist is supplied; takes precedence when both are provided. This is not a Recoup artist/account ID.", "schema": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", "example": "artist_123" } } ], "responses": { "200": { "description": "Artist profile", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchProfileResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Insufficient research credits. The response includes a static billingUrl; no checkout session is created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" } } } }, "403": { "description": "Credential, account or provider permission denied.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "404": { "description": "Artist/track resolution or requested provider data was not found. A failed name search may be reported as 404; retry with an exact provider ID when known.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "429": { "description": "Provider rate or quota limit. The adapter returns an error status but does not forward provider Retry-After headers. Avoid an unbounded retry loop.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "500": { "description": "Missing provider configuration, transport failure or internal error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" }, "502": { "description": "Upstream provider failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "504": { "description": "the configured research data connection request timed out.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } } } } } }, "components": { "schemas": { "ResearchProfileResponse": { "type": "object", "description": "Full artist profile. For longitudinal platform metrics call `GET /api/research/metrics`.", "properties": { "status": { "type": "string", "example": "success" }, "result": { "type": "string", "example": "success" }, "message": { "type": "string", "example": "Data Retrieved." }, "artist_info": { "$ref": "#/components/schemas/ResearchArtistInfo" } }, "additionalProperties": true }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] }, "ResearchArtistInfo": { "type": "object", "description": "Full artist record — identity, bio, genres, platform links, and related artists.", "properties": { "name": { "type": "string" }, "avatar": { "type": "string" }, "site_url": { "type": "string" }, "country": { "type": "string", "description": "ISO country code.", "nullable": true }, "bio": { "type": "string", "nullable": true }, "genres": { "type": "array", "items": { "type": "string" } }, "links": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchLink" } }, "related_artists": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchArtistRef" } } }, "additionalProperties": true }, "ResearchLink": { "type": "object", "description": "External platform link.", "properties": { "source": { "type": "string", "description": "Platform name, e.g. `spotify`, `apple_music`." }, "external_id": { "type": "string", "description": "Platform-specific ID." }, "url": { "type": "string" }, "isrc": { "type": "string", "description": "ISRC (tracks only)." } }, "additionalProperties": true }, "ResearchArtistRef": { "type": "object", "description": "Provider-neutral artist reference.", "properties": { "id": { "type": "string", "description": "Provider-neutral artist ID.", "example": "wjcgfd9i" }, "name": { "type": "string", "example": "Drake" }, "avatar": { "type": "string", "description": "Artist image URL.", "nullable": true }, "site_url": { "type": "string", "description": "Artist page URL.", "nullable": true } }, "additionalProperties": true } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # Platform Metrics Source: https://recoupable.dev/docs/api-reference/research/metrics Read current artist statistics for the required platform `source`. This is a synchronous snapshot, without a historical series or refresh-pending job. A valid source does not guarantee data is available. Save dated responses to compare results over time. ## GET /api/research/metrics Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/metrics": { "get": { "description": "Read current artist statistics for the required platform `source`. This is a synchronous snapshot, without a historical series or refresh-pending job. A valid source does not guarantee data is available. Save dated responses to compare results over time.\n\nUse your Recoup API key (`x-api-key`) or supported bearer credential. Successful artist and track research reads cost $0.05 (50,000 micro-dollar credits). Recoup manages the data connections. Coverage and rate limits vary; see [Research availability](/docs/research-availability).", "parameters": [ { "name": "artist", "in": "query", "required": false, "description": "Artist name; Recoup resolves the first provider search match. Required unless id is supplied. Recoup artist UUID resolution is not implemented here.", "schema": { "type": "string" } }, { "name": "source", "in": "query", "required": true, "description": "Platform to get metrics for.", "schema": { "type": "string", "enum": [ "spotify", "instagram", "tiktok", "twitter", "facebook", "youtube_channel", "youtube_artist", "soundcloud", "deezer", "twitch", "line", "melon", "wikipedia", "bandsintown", "radio", "sxm" ] } }, { "name": "id", "in": "query", "required": false, "description": "Research artist ID returned by search or lookup; treat it as an opaque string. Required unless artist is supplied; takes precedence when both are provided. This is not a Recoup artist/account ID.", "schema": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", "example": "artist_123" } } ], "responses": { "200": { "description": "Platform metrics over time", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchMetricsResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Insufficient research credits. The response includes a static billingUrl; no checkout session is created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" } } } }, "403": { "description": "Credential, account or provider permission denied.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "404": { "description": "Artist/track resolution or requested provider data was not found. A failed name search may be reported as 404; retry with an exact provider ID when known.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "429": { "description": "Provider rate or quota limit. The adapter returns an error status but does not forward provider Retry-After headers. Avoid an unbounded retry loop.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "500": { "description": "Missing provider configuration, transport failure or internal error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" }, "502": { "description": "Upstream provider failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "504": { "description": "the configured research data connection request timed out.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } } } } } }, "components": { "schemas": { "ResearchMetricsResponse": { "type": "object", "description": "Current statistics for the artist on the requested `source`.", "properties": { "status": { "type": "string", "example": "success" }, "result": { "type": "string", "example": "success" }, "message": { "type": "string", "example": "Data Retrieved." }, "stats": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchMetricStat" } }, "artist_info": { "$ref": "#/components/schemas/ResearchArtistRef" }, "source_ids": { "type": "array", "description": "Sources included in `stats`.", "items": { "type": "string" } } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] }, "ResearchMetricStat": { "type": "object", "description": "Per-source current statistics.", "properties": { "source": { "type": "string", "description": "Platform the metrics belong to, e.g. `spotify`, `tiktok`." }, "data": { "type": "object", "description": "Platform-specific metric fields. Keys vary by `source`.", "additionalProperties": true, "example": { "monthly_listeners_current": 99222567, "followers_total": 112035525, "popularity_current": 100, "streams_total": 137582496099, "playlists_current": 169220, "charts_current": 184 } } }, "additionalProperties": true }, "ResearchArtistRef": { "type": "object", "description": "Provider-neutral artist reference.", "properties": { "id": { "type": "string", "description": "Provider-neutral artist ID.", "example": "wjcgfd9i" }, "name": { "type": "string", "example": "Drake" }, "avatar": { "type": "string", "description": "Artist image URL.", "nullable": true }, "site_url": { "type": "string", "description": "Artist page URL.", "nullable": true } }, "additionalProperties": true } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # Audience Demographics Source: https://recoupable.dev/docs/api-reference/research/audience Read available audience data for an artist and platform. The default is `instagram`; other supported data connections can include Spotify, TikTok and YouTube. Geographic and demographic coverage varies. Age and gender breakdowns are not guaranteed. ## GET /api/research/audience Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/audience": { "get": { "description": "Read available audience data for an artist and platform. The default is `instagram`; other supported data connections can include Spotify, TikTok and YouTube. Geographic and demographic coverage varies. Age and gender breakdowns are not guaranteed.\n\nUse your Recoup API key (`x-api-key`) or supported bearer credential. Successful artist and track research reads cost $0.05 (50,000 micro-dollar credits). Recoup manages the data connections. Coverage and rate limits vary; see [Research availability](/docs/research-availability).", "parameters": [ { "name": "artist", "in": "query", "required": false, "description": "Artist name; Recoup resolves the first provider search match. Required unless id is supplied. Recoup artist UUID resolution is not implemented here.", "schema": { "type": "string" } }, { "name": "platform", "in": "query", "required": false, "description": "Platform mapped to a provider audience metric source. Common values include instagram, tiktok, spotify, youtube_artist and youtube_channel. Unmapped strings pass through; provider support applies.", "schema": { "type": "string", "default": "instagram" } }, { "name": "id", "in": "query", "required": false, "description": "Research artist ID returned by search or lookup; treat it as an opaque string. Required unless artist is supplied; takes precedence when both are provided. This is not a Recoup artist/account ID.", "schema": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", "example": "artist_123" } } ], "responses": { "200": { "description": "Audience demographics", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchAudienceResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Insufficient research credits. The response includes a static billingUrl; no checkout session is created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" } } } }, "403": { "description": "Credential, account or provider permission denied.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "404": { "description": "Artist/track resolution or requested provider data was not found. A failed name search may be reported as 404; retry with an exact provider ID when known.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "429": { "description": "Provider rate or quota limit. The adapter returns an error status but does not forward provider Retry-After headers. Avoid an unbounded retry loop.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "500": { "description": "Missing provider configuration, transport failure or internal error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" }, "502": { "description": "Upstream provider failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "504": { "description": "the configured research data connection request timed out.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } } } } } }, "components": { "schemas": { "ResearchAudienceResponse": { "type": "object", "description": "Provider audience payload. Available geographic and demographic fields vary by source and entitlement.", "properties": { "status": { "type": "string", "example": "success" }, "result": { "type": "string", "example": "success" }, "message": { "type": "string", "example": "Data Retrieved." }, "audience": { "type": "array", "description": "Provider audience entries; fields vary by requested metric source. Empty data does not establish an absent audience.", "items": { "type": "object", "additionalProperties": true } }, "artist_info": { "$ref": "#/components/schemas/ResearchArtistRef" }, "source_ids": { "type": "array", "items": { "type": "string" } } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] }, "ResearchArtistRef": { "type": "object", "description": "Provider-neutral artist reference.", "properties": { "id": { "type": "string", "description": "Provider-neutral artist ID.", "example": "wjcgfd9i" }, "name": { "type": "string", "example": "Drake" }, "avatar": { "type": "string", "description": "Artist image URL.", "nullable": true }, "site_url": { "type": "string", "description": "Artist page URL.", "nullable": true } }, "additionalProperties": true } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # Insights Activities Source: https://recoupable.dev/docs/api-reference/research/insights Read available artist activities under `insights`. This endpoint returns activity data; it does not generate an AI analysis or a separate insight report. ## GET /api/research/insights Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/insights": { "get": { "description": "Read available artist activities under `insights`. This endpoint returns activity data; it does not generate an AI analysis or a separate insight report.\n\nUse your Recoup API key (`x-api-key`) or supported bearer credential. Successful artist and track research reads cost $0.05 (50,000 micro-dollar credits). Recoup manages the data connections. Coverage and rate limits vary; see [Research availability](/docs/research-availability).", "parameters": [ { "name": "artist", "in": "query", "required": false, "description": "Artist name; Recoup resolves the first provider search match. Required unless id is supplied. Recoup artist UUID resolution is not implemented here.", "schema": { "type": "string" } }, { "name": "id", "in": "query", "required": false, "description": "Research artist ID returned by search or lookup; treat it as an opaque string. Required unless artist is supplied; takes precedence when both are provided. This is not a Recoup artist/account ID.", "schema": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", "example": "artist_123" } } ], "responses": { "200": { "description": "AI-generated insights", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsightsResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Insufficient research credits. The response includes a static billingUrl; no checkout session is created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" } } } }, "403": { "description": "Credential, account or provider permission denied.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "404": { "description": "Artist/track resolution or requested provider data was not found. A failed name search may be reported as 404; retry with an exact provider ID when known.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "429": { "description": "Provider rate or quota limit. The adapter returns an error status but does not forward provider Retry-After headers. Avoid an unbounded retry loop.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "500": { "description": "Missing provider configuration, transport failure or internal error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" }, "502": { "description": "Upstream provider failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "504": { "description": "the configured research data connection request timed out.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } } } } } }, "components": { "schemas": { "ResearchInsightsResponse": { "type": "object", "description": "Noteworthy activity highlights.", "properties": { "status": { "type": "string", "example": "success" }, "insights": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchActivity" } } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] }, "ResearchActivity": { "type": "object", "description": "Dated activity event. Shared by career, insights, and milestones.", "properties": { "source": { "type": "string", "description": "Platform, e.g. `spotify`." }, "activity_text": { "type": "string", "description": "Human-readable activity description." }, "activity_type": { "type": "string", "description": "e.g. `playlist`, `chart`." }, "activity_date": { "type": "string", "example": "2026-06-04" }, "activity_url": { "type": "string", "nullable": true }, "activity_avatar": { "type": "string", "nullable": true }, "activity_tier": { "type": "integer", "description": "Importance tier (lower = more significant)." }, "track_info": { "$ref": "#/components/schemas/ResearchActivityTrackInfo" } }, "additionalProperties": true }, "ResearchActivityTrackInfo": { "type": "object", "description": "Track context attached to an activity entry.", "properties": { "title": { "type": "string" }, "avatar": { "type": "string" }, "release_date": { "type": "string", "nullable": true }, "site_url": { "type": "string" }, "artists": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchArtistMini" } } }, "additionalProperties": true }, "ResearchArtistMini": { "type": "object", "description": "Minimal artist reference embedded in catalog/activity payloads.", "properties": { "name": { "type": "string" } }, "additionalProperties": true } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # Career Timeline Source: https://recoupable.dev/docs/api-reference/research/career Read available dated artist activities under `career`. Career, insights and milestones use the same activity feed. This endpoint does not calculate a career stage or score, and does not guarantee a complete career history. ## GET /api/research/career Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/career": { "get": { "description": "Read available dated artist activities under `career`. Career, insights and milestones use the same activity feed. This endpoint does not calculate a career stage or score, and does not guarantee a complete career history.\n\nUse your Recoup API key (`x-api-key`) or supported bearer credential. Successful artist and track research reads cost $0.05 (50,000 micro-dollar credits). Recoup manages the data connections. Coverage and rate limits vary; see [Research availability](/docs/research-availability).", "parameters": [ { "name": "artist", "in": "query", "required": false, "description": "Artist name; Recoup resolves the first provider search match. Required unless id is supplied. Recoup artist UUID resolution is not implemented here.", "schema": { "type": "string" } }, { "name": "id", "in": "query", "required": false, "description": "Research artist ID returned by search or lookup; treat it as an opaque string. Required unless artist is supplied; takes precedence when both are provided. This is not a Recoup artist/account ID.", "schema": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", "example": "artist_123" } } ], "responses": { "200": { "description": "Career timeline", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchCareerResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Insufficient research credits. The response includes a static billingUrl; no checkout session is created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" } } } }, "403": { "description": "Credential, account or provider permission denied.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "404": { "description": "Artist/track resolution or requested provider data was not found. A failed name search may be reported as 404; retry with an exact provider ID when known.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "429": { "description": "Provider rate or quota limit. The adapter returns an error status but does not forward provider Retry-After headers. Avoid an unbounded retry loop.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "500": { "description": "Missing provider configuration, transport failure or internal error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" }, "502": { "description": "Upstream provider failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "504": { "description": "the configured research data connection request timed out.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } } } } } }, "components": { "schemas": { "ResearchCareerResponse": { "type": "object", "description": "Career activity feed.", "properties": { "status": { "type": "string", "example": "success" }, "career": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchActivity" } } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] }, "ResearchActivity": { "type": "object", "description": "Dated activity event. Shared by career, insights, and milestones.", "properties": { "source": { "type": "string", "description": "Platform, e.g. `spotify`." }, "activity_text": { "type": "string", "description": "Human-readable activity description." }, "activity_type": { "type": "string", "description": "e.g. `playlist`, `chart`." }, "activity_date": { "type": "string", "example": "2026-06-04" }, "activity_url": { "type": "string", "nullable": true }, "activity_avatar": { "type": "string", "nullable": true }, "activity_tier": { "type": "integer", "description": "Importance tier (lower = more significant)." }, "track_info": { "$ref": "#/components/schemas/ResearchActivityTrackInfo" } }, "additionalProperties": true }, "ResearchActivityTrackInfo": { "type": "object", "description": "Track context attached to an activity entry.", "properties": { "title": { "type": "string" }, "avatar": { "type": "string" }, "release_date": { "type": "string", "nullable": true }, "site_url": { "type": "string" }, "artists": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchArtistMini" } } }, "additionalProperties": true }, "ResearchArtistMini": { "type": "object", "description": "Minimal artist reference embedded in catalog/activity payloads.", "properties": { "name": { "type": "string" } }, "additionalProperties": true } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # Milestones Source: https://recoupable.dev/docs/api-reference/research/milestones Read available artist activities under `milestones`. Coverage varies; an empty result does not prove the artist has no milestones. This endpoint does not guarantee a complete history or scored career timeline. ## GET /api/research/milestones Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/milestones": { "get": { "description": "Read available artist activities under `milestones`. Coverage varies; an empty result does not prove the artist has no milestones. This endpoint does not guarantee a complete history or scored career timeline.\n\nUse your Recoup API key (`x-api-key`) or supported bearer credential. Successful artist and track research reads cost $0.05 (50,000 micro-dollar credits). Recoup manages the data connections. Coverage and rate limits vary; see [Research availability](/docs/research-availability).", "parameters": [ { "name": "artist", "in": "query", "required": false, "description": "Artist name; Recoup resolves the first provider search match. Required unless id is supplied. Recoup artist UUID resolution is not implemented here.", "schema": { "type": "string" } }, { "name": "id", "in": "query", "required": false, "description": "Research artist ID returned by search or lookup; treat it as an opaque string. Required unless artist is supplied; takes precedence when both are provided. This is not a Recoup artist/account ID.", "schema": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", "example": "artist_123" } } ], "responses": { "200": { "description": "Milestone list", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchMilestonesResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Insufficient research credits. The response includes a static billingUrl; no checkout session is created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" } } } }, "403": { "description": "Credential, account or provider permission denied.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "404": { "description": "Artist/track resolution or requested provider data was not found. A failed name search may be reported as 404; retry with an exact provider ID when known.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "429": { "description": "Provider rate or quota limit. The adapter returns an error status but does not forward provider Retry-After headers. Avoid an unbounded retry loop.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "500": { "description": "Missing provider configuration, transport failure or internal error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" }, "502": { "description": "Upstream provider failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "504": { "description": "the configured research data connection request timed out.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } } } } } }, "components": { "schemas": { "ResearchMilestonesResponse": { "type": "object", "description": "Milestone activity entries (same shape as career/insights).", "properties": { "status": { "type": "string", "example": "success" }, "milestones": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchActivity" } } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] }, "ResearchActivity": { "type": "object", "description": "Dated activity event. Shared by career, insights, and milestones.", "properties": { "source": { "type": "string", "description": "Platform, e.g. `spotify`." }, "activity_text": { "type": "string", "description": "Human-readable activity description." }, "activity_type": { "type": "string", "description": "e.g. `playlist`, `chart`." }, "activity_date": { "type": "string", "example": "2026-06-04" }, "activity_url": { "type": "string", "nullable": true }, "activity_avatar": { "type": "string", "nullable": true }, "activity_tier": { "type": "integer", "description": "Importance tier (lower = more significant)." }, "track_info": { "$ref": "#/components/schemas/ResearchActivityTrackInfo" } }, "additionalProperties": true }, "ResearchActivityTrackInfo": { "type": "object", "description": "Track context attached to an activity entry.", "properties": { "title": { "type": "string" }, "avatar": { "type": "string" }, "release_date": { "type": "string", "nullable": true }, "site_url": { "type": "string" }, "artists": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchArtistMini" } } }, "additionalProperties": true }, "ResearchArtistMini": { "type": "object", "description": "Minimal artist reference embedded in catalog/activity payloads.", "properties": { "name": { "type": "string" } }, "additionalProperties": true } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # Artist Events Source: https://recoupable.dev/docs/api-reference/research/events List an artist's live shows. Returns one row per event with venue, city, country, ticket link, and lineup. ## POST /api/research/events Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/events": { "post": { "description": "List an artist's live shows. Returns one row per event with venue, city, country, ticket link, and lineup.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchEventsRequest" } } } }, "responses": { "200": { "description": "Events for the artist. An artist with no matching events returns an empty `events` array, not an error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchEventsResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Insufficient research credits — the body includes a static `billingUrl`. No Stripe object is created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" } } } }, "404": { "description": "The artist has no live-events profile connected, so no events can be looked up. Distinct from a 200 with an empty `events` array, which means the artist is connected but has no matching events.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "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" } } } } } } } }, "components": { "schemas": { "ResearchEventsRequest": { "type": "object", "required": [ "artist_id" ], "description": "Request body for artist event lookup.", "properties": { "artist_id": { "type": "string", "format": "uuid", "description": "Recoup artist id. Events are resolved through the live-events profile connected to this artist, so the lookup is exact and cannot drift to a same-named performer. Returns 404 if no live-events profile is connected.", "example": "123694f2-1dab-40b4-8a75-84d39571c0bc" }, "date": { "type": "string", "enum": [ "upcoming", "past", "all" ], "default": "upcoming", "description": "Which events to return." } } }, "ResearchEventsResponse": { "type": "object", "required": [ "status", "events" ], "description": "Live events for one artist.", "properties": { "status": { "type": "string", "example": "success" }, "events": { "type": "array", "description": "Matching events, ascending by date. Empty when the artist has none.", "items": { "type": "object", "properties": { "date": { "type": "string", "description": "Event date as ISO 8601 (YYYY-MM-DD).", "example": "2026-09-26" }, "venue": { "type": "string", "description": "Venue name.", "example": "O2 Academy Brixton" }, "city": { "type": "string", "example": "London" }, "region": { "type": "string", "nullable": true, "description": "State or region where the source provides one. US venues carry a state code such as \"TN\"; most non-US venues return an empty string, as in the example below.", "example": "" }, "country": { "type": "string", "description": "Country name. Pair with `city` before filtering by market, since city names such as London and Paris are not unique across countries.", "example": "United Kingdom" }, "ticket_url": { "type": "string", "nullable": true, "description": "Ticket link where one is published." }, "sold_out": { "type": "boolean", "description": "Whether the source marks the event sold out." }, "lineup": { "type": "array", "description": "Billed artists, the queried artist first. More than one entry means a shared bill.", "items": { "type": "string" } } } } } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] } } } } ``` # Similar Artists Source: https://recoupable.dev/docs/api-reference/research/similar Read unweighted related artists. The accepted `audience`, `genre`, `mood` and `musicality` values (high/medium/low) do not currently change the ranking. No similarity score is calculated. A positive integer `limit` slices the returned list; invalid or non-positive limits are ignored. ## GET /api/research/similar Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/similar": { "get": { "description": "Read unweighted related artists. The accepted `audience`, `genre`, `mood` and `musicality` values (high/medium/low) do not currently change the ranking. No similarity score is calculated. A positive integer `limit` slices the returned list; invalid or non-positive limits are ignored.\n\nUse your Recoup API key (`x-api-key`) or supported bearer credential. Successful artist and track research reads cost $0.05 (50,000 micro-dollar credits). Recoup manages the data connections. Coverage and rate limits vary; see [Research availability](/docs/research-availability).", "parameters": [ { "name": "artist", "in": "query", "required": false, "description": "Artist name; Recoup resolves the first provider search match. Required unless id is supplied. Recoup artist UUID resolution is not implemented here.", "schema": { "type": "string" } }, { "name": "audience", "in": "query", "required": false, "description": "Audience overlap weight.", "schema": { "type": "string", "enum": [ "high", "medium", "low" ], "default": "medium" } }, { "name": "genre", "in": "query", "required": false, "description": "Genre similarity weight.", "schema": { "type": "string", "enum": [ "high", "medium", "low" ], "default": "medium" } }, { "name": "mood", "in": "query", "required": false, "description": "Mood similarity weight.", "schema": { "type": "string", "enum": [ "high", "medium", "low" ], "default": "medium" } }, { "name": "musicality", "in": "query", "required": false, "description": "Musicality similarity weight.", "schema": { "type": "string", "enum": [ "high", "medium", "low" ], "default": "medium" } }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Optional raw string; no Recoup default. For similar artists, only a positive integer limits the returned list; for playlists it passes through to the provider." }, { "name": "id", "in": "query", "required": false, "description": "Research artist ID returned by search or lookup; treat it as an opaque string. Required unless artist is supplied; takes precedence when both are provided. This is not a Recoup artist/account ID.", "schema": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", "example": "artist_123" } } ], "responses": { "200": { "description": "Unweighted related artists with provider fields; no overlap scores are computed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchSimilarResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Insufficient research credits. The response includes a static billingUrl; no checkout session is created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" } } } }, "403": { "description": "Credential, account or provider permission denied.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "404": { "description": "Artist/track resolution or requested provider data was not found. A failed name search may be reported as 404; retry with an exact provider ID when known.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "429": { "description": "Provider rate or quota limit. The adapter returns an error status but does not forward provider Retry-After headers. Avoid an unbounded retry loop.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "500": { "description": "Missing provider configuration, transport failure or internal error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" }, "502": { "description": "Upstream provider failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "504": { "description": "the configured research data connection request timed out.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } } } } } }, "components": { "schemas": { "ResearchSimilarResponse": { "type": "object", "description": "Similar artists. Similarity is unweighted (no score/ranking fields).", "properties": { "status": { "type": "string", "example": "success" }, "artists": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchArtistRef" } }, "total": { "type": "integer", "description": "Only present if the adapter receives a provider total; not guaranteed." } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] }, "ResearchArtistRef": { "type": "object", "description": "Provider-neutral artist reference.", "properties": { "id": { "type": "string", "description": "Provider-neutral artist ID.", "example": "wjcgfd9i" }, "name": { "type": "string", "example": "Drake" }, "avatar": { "type": "string", "description": "Artist image URL.", "nullable": true }, "site_url": { "type": "string", "description": "Artist page URL.", "nullable": true } }, "additionalProperties": true } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # Social URLs Source: https://recoupable.dev/docs/api-reference/research/urls Read available platform URLs as `{domain, url}` entries. A missing link means no matching URL was returned; it does not prove the artist lacks an account on that platform. ## GET /api/research/urls Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/urls": { "get": { "description": "Read available platform URLs as `{domain, url}` entries. A missing link means no matching URL was returned; it does not prove the artist lacks an account on that platform.\n\nUse your Recoup API key (`x-api-key`) or supported bearer credential. Successful artist and track research reads cost $0.05 (50,000 micro-dollar credits). Recoup manages the data connections. Coverage and rate limits vary; see [Research availability](/docs/research-availability).", "parameters": [ { "name": "artist", "in": "query", "required": false, "description": "Artist name; Recoup resolves the first provider search match. Required unless id is supplied. Recoup artist UUID resolution is not implemented here.", "schema": { "type": "string" } }, { "name": "id", "in": "query", "required": false, "description": "Research artist ID returned by search or lookup; treat it as an opaque string. Required unless artist is supplied; takes precedence when both are provided. This is not a Recoup artist/account ID.", "schema": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", "example": "artist_123" } } ], "responses": { "200": { "description": "Social and streaming URLs", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchUrlsResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Insufficient research credits. The response includes a static billingUrl; no checkout session is created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" } } } }, "403": { "description": "Credential, account or provider permission denied.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "404": { "description": "Artist/track resolution or requested provider data was not found. A failed name search may be reported as 404; retry with an exact provider ID when known.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "429": { "description": "Provider rate or quota limit. The adapter returns an error status but does not forward provider Retry-After headers. Avoid an unbounded retry loop.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "500": { "description": "Missing provider configuration, transport failure or internal error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" }, "502": { "description": "Upstream provider failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "504": { "description": "the configured research data connection request timed out.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } } } } } }, "components": { "schemas": { "ResearchUrlsResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "example": "success" }, "urls": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchUrlEntry" } } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] }, "ResearchUrlEntry": { "type": "object", "properties": { "domain": { "type": "string", "description": "Platform name (e.g., spotify, instagram)." }, "url": { "type": "string", "format": "uri" } } } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # Albums Source: https://recoupable.dev/docs/api-reference/research/albums Read the artist catalog through the `albums` view using the `artist_id` selected from [Search](/docs/api-reference/research/search). Results can include track-shaped catalog entries rather than distinct album objects. `is_primary` accepts true/false; filtering depends on the data connection. `limit` must be a positive integer string and `offset` a non-negative integer string. ## GET /api/research/albums Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/albums": { "get": { "description": "Read the artist catalog through the `albums` view using the `artist_id` selected from [Search](/docs/api-reference/research/search). Results can include track-shaped catalog entries rather than distinct album objects. `is_primary` accepts true/false; filtering depends on the data connection. `limit` must be a positive integer string and `offset` a non-negative integer string.\n\nUse your Recoup API key (`x-api-key`) or supported bearer credential. Successful artist and track research reads cost $0.05 (50,000 micro-dollar credits). Recoup manages the data connections. Coverage and rate limits vary; see [Research availability](/docs/research-availability).", "parameters": [ { "name": "artist_id", "in": "query", "required": true, "description": "Provider artist ID. Obtain it from search or lookup results.", "schema": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", "example": "artist_123" } }, { "name": "is_primary", "in": "query", "required": false, "description": "When `true` (default), returns only albums where the artist is a main artist. Set to `false` to include feature appearances, DJ compilations, and soundtracks.", "schema": { "type": "string", "enum": [ "true", "false" ], "default": "true" } }, { "name": "limit", "in": "query", "required": false, "description": "Number of albums to return. Defaults to the provider's default when omitted.", "schema": { "type": "string", "pattern": "^[1-9][0-9]*$" } }, { "name": "offset", "in": "query", "required": false, "description": "Pagination offset. Defaults to 0 when omitted.", "schema": { "type": "string", "pattern": "^(0|[1-9][0-9]*)$" } } ], "responses": { "200": { "description": "Artist albums", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchAlbumsResponse" } } } }, "400": { "description": "Validation error — `artist_id` missing or invalid", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Insufficient research credits. The response includes a static billingUrl; no checkout session is created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" } } } }, "403": { "description": "Credential, account or provider permission denied.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "404": { "description": "Artist/track resolution or requested provider data was not found. A failed name search may be reported as 404; retry with an exact provider ID when known.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "429": { "description": "Provider rate or quota limit. The adapter returns an error status but does not forward provider Retry-After headers. Avoid an unbounded retry loop.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "500": { "description": "Missing provider configuration, transport failure or internal error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" }, "502": { "description": "Upstream provider failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "504": { "description": "the configured research data connection request timed out.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } } } } } }, "components": { "schemas": { "ResearchAlbumsResponse": { "type": "object", "description": "Artist catalog (albums view).", "properties": { "status": { "type": "string", "example": "success" }, "albums": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchCatalogItem" } } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] }, "ResearchCatalogItem": { "type": "object", "description": "Catalog entry. Unfiltered — high-catalog artists include remixes/features.", "properties": { "title": { "type": "string" }, "avatar": { "type": "string" }, "release_date": { "type": "string", "nullable": true }, "site_url": { "type": "string" }, "isrcs": { "type": "array", "items": { "type": "string" } }, "artists": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchArtistMini" } } }, "additionalProperties": true }, "ResearchArtistMini": { "type": "object", "description": "Minimal artist reference embedded in catalog/activity payloads.", "properties": { "name": { "type": "string" } }, "additionalProperties": true } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # Tracks Source: https://recoupable.dev/docs/api-reference/research/tracks Read available artist catalog entries under `tracks`. Entries can include remixes and features; completeness and fields vary. This endpoint accepts an artist identifier without catalog pagination or filtering controls. ## GET /api/research/tracks Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/tracks": { "get": { "description": "Read available artist catalog entries under `tracks`. Entries can include remixes and features; completeness and fields vary. This endpoint accepts an artist identifier without catalog pagination or filtering controls.\n\nUse your Recoup API key (`x-api-key`) or supported bearer credential. Successful artist and track research reads cost $0.05 (50,000 micro-dollar credits). Recoup manages the data connections. Coverage and rate limits vary; see [Research availability](/docs/research-availability).", "parameters": [ { "name": "artist", "in": "query", "required": false, "description": "Artist name; Recoup resolves the first provider search match. Required unless id is supplied. Recoup artist UUID resolution is not implemented here.", "schema": { "type": "string" } }, { "name": "id", "in": "query", "required": false, "description": "Research artist ID returned by search or lookup; treat it as an opaque string. Required unless artist is supplied; takes precedence when both are provided. This is not a Recoup artist/account ID.", "schema": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", "example": "artist_123" } } ], "responses": { "200": { "description": "Artist tracks", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchTracksResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Insufficient research credits. The response includes a static billingUrl; no checkout session is created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" } } } }, "403": { "description": "Credential, account or provider permission denied.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "404": { "description": "Artist/track resolution or requested provider data was not found. A failed name search may be reported as 404; retry with an exact provider ID when known.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "429": { "description": "Provider rate or quota limit. The adapter returns an error status but does not forward provider Retry-After headers. Avoid an unbounded retry loop.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "500": { "description": "Missing provider configuration, transport failure or internal error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" }, "502": { "description": "Upstream provider failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "504": { "description": "the configured research data connection request timed out.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } } } } } }, "components": { "schemas": { "ResearchTracksResponse": { "type": "object", "description": "Artist catalog (tracks view).", "properties": { "status": { "type": "string", "example": "success" }, "tracks": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchCatalogItem" } } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] }, "ResearchCatalogItem": { "type": "object", "description": "Catalog entry. Unfiltered — high-catalog artists include remixes/features.", "properties": { "title": { "type": "string" }, "avatar": { "type": "string" }, "release_date": { "type": "string", "nullable": true }, "site_url": { "type": "string" }, "isrcs": { "type": "array", "items": { "type": "string" } }, "artists": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchArtistMini" } } }, "additionalProperties": true }, "ResearchArtistMini": { "type": "object", "description": "Minimal artist reference embedded in catalog/activity payloads.", "properties": { "name": { "type": "string" } }, "additionalProperties": true } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # Track Source: https://recoupable.dev/docs/api-reference/research/track Read track details using the `id` selected from [Search](/docs/api-reference/research/search) with `type=tracks`. Treat this ID as an opaque string, separate from a Recoup account UUID. Track metadata and available fields vary by recording. ## GET /api/research/track Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/track": { "get": { "description": "Read track details using the `id` selected from [Search](/docs/api-reference/research/search) with `type=tracks`. Treat this ID as an opaque string, separate from a Recoup account UUID. Track metadata and available fields vary by recording.\n\nUse your Recoup API key (`x-api-key`) or supported bearer credential. Successful artist and track research reads cost $0.05 (50,000 micro-dollar credits). Recoup manages the data connections. Coverage and rate limits vary; see [Research availability](/docs/research-availability).", "parameters": [ { "name": "id", "in": "query", "required": true, "description": "Provider track ID. Obtain it from search results or a track lookup.", "schema": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", "example": "track_123" } } ], "responses": { "200": { "description": "Track metadata", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchTrackResponse" } } } }, "400": { "description": "Missing id or invalid provider ID string pattern.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Insufficient research credits. The response includes a static billingUrl; no checkout session is created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" } } } }, "403": { "description": "Credential, account or provider permission denied.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "404": { "description": "Artist/track resolution or requested provider data was not found. A failed name search may be reported as 404; retry with an exact provider ID when known.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "429": { "description": "Provider rate or quota limit. The adapter returns an error status but does not forward provider Retry-After headers. Avoid an unbounded retry loop.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "500": { "description": "Missing provider configuration, transport failure or internal error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" }, "502": { "description": "Upstream provider failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "504": { "description": "the configured research data connection request timed out.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } } } } } }, "components": { "schemas": { "ResearchTrackResponse": { "type": "object", "description": "Track metadata and audio analysis.", "properties": { "status": { "type": "string", "example": "success" }, "result": { "type": "string", "example": "success" }, "message": { "type": "string", "example": "Data Retrieved." }, "track_info": { "$ref": "#/components/schemas/ResearchTrackInfo" }, "audio_analysis": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchAudioAnalysisEntry" } } }, "additionalProperties": true }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] }, "ResearchTrackInfo": { "type": "object", "description": "Full track record.", "properties": { "title": { "type": "string" }, "avatar": { "type": "string" }, "site_url": { "type": "string" }, "release_date": { "type": "string", "nullable": true }, "artists": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchArtistMini" } }, "is_remix": { "type": "boolean" }, "collaborators": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchCollaborator" } }, "labels": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, "distributors": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" } }, "additionalProperties": true } }, "genres": { "type": "array", "items": { "type": "string" } }, "links": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchLink" } } }, "additionalProperties": true }, "ResearchAudioAnalysisEntry": { "type": "object", "properties": { "key": { "type": "string", "description": "Audio feature name, e.g. `acousticness`, `tempo`." }, "value": { "type": "string", "description": "Feature value (string-encoded)." } } }, "ResearchArtistMini": { "type": "object", "description": "Minimal artist reference embedded in catalog/activity payloads.", "properties": { "name": { "type": "string" } }, "additionalProperties": true }, "ResearchCollaborator": { "type": "object", "properties": { "name": { "type": "string" }, "roles": { "type": "array", "items": { "type": "string" } } }, "additionalProperties": true }, "ResearchLink": { "type": "object", "description": "External platform link.", "properties": { "source": { "type": "string", "description": "Platform name, e.g. `spotify`, `apple_music`." }, "external_id": { "type": "string", "description": "Platform-specific ID." }, "url": { "type": "string" }, "isrc": { "type": "string", "description": "ISRC (tracks only)." } }, "additionalProperties": true } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # Track Stats Source: https://recoupable.dev/docs/api-reference/research/track-stats Read current track statistics using exactly one of `isrc`, `spotify_track_id` or `apple_music_track_id`, plus the required platform `source`. Recoup uses stored Spotify measurements when available for an ISRC and otherwise requests current research data. Fields and coverage vary. Preserve returned `data_source` and capture timestamps; missing provenance remains unknown. ## GET /api/research/track/stats Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/track/stats": { "get": { "description": "Read current track statistics using exactly one of `isrc`, `spotify_track_id` or `apple_music_track_id`, plus the required platform `source`. Recoup uses stored Spotify measurements when available for an ISRC and otherwise requests current research data. Fields and coverage vary. Preserve returned `data_source` and capture timestamps; missing provenance remains unknown.", "parameters": [ { "name": "isrc", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Supply exactly one track identifier.", "example": "USQY51771120" }, { "name": "spotify_track_id", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Supply exactly one track identifier." }, { "name": "apple_music_track_id", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Supply exactly one track identifier." }, { "name": "source", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Required provider source or comma-separated sources; not limited to Spotify.", "example": "spotify" }, { "name": "with_playlists", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Optional raw string forwarded unchanged to the configured research data connection. Use true or false per the provider contract; Recoup does not validate or coerce this flag.", "example": "true" }, { "name": "with_charts", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Optional raw string forwarded unchanged to the configured research data connection. Use true or false per the provider contract; Recoup does not validate or coerce this flag.", "example": "true" }, { "name": "with_stations", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Optional raw string forwarded unchanged to the configured research data connection. Use true or false per the provider contract; Recoup does not validate or coerce this flag.", "example": "true" }, { "name": "with_videos", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Optional raw string forwarded unchanged to the configured research data connection. Use true or false per the provider contract; Recoup does not validate or coerce this flag.", "example": "true" }, { "name": "with_links", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Optional raw string forwarded unchanged to the configured research data connection. Use true or false per the provider contract; Recoup does not validate or coerce this flag.", "example": "true" }, { "name": "only_current", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Optional raw string forwarded unchanged to the configured research data connection. Use true or false per the provider contract; Recoup does not validate or coerce this flag.", "example": "true" }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Optional raw pagination string forwarded unchanged to the configured research data connection. Recoup does not validate its numeric range; provider limits apply.", "example": "50" }, { "name": "offset", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Optional raw pagination string forwarded unchanged to the configured research data connection. Recoup does not validate its numeric range; provider limits apply.", "example": "0" } ], "responses": { "200": { "description": "Current per-source track statistics from the measurement store or the configured research data connection, with data_source on recognized stats entries.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchTrackStatsResponse" } } } }, "400": { "description": "Missing source, or zero/multiple track identifiers. Supply exactly one of the four supported identifiers.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Insufficient research credits — the body includes a static `billingUrl`. No Stripe object is created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" } } } }, "404": { "description": "Requested track or source data was not found by the provider.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "429": { "description": "Provider rate/quota limit; do not retry in an unbounded loop." }, "500": { "description": "Missing provider configuration, transport failure or internal error." }, "504": { "description": "the configured research data connection request timed out." } } } } }, "components": { "schemas": { "ResearchTrackStatsResponse": { "type": "object", "description": "Current per-source track statistics from the Spotify measurement store or the configured research data connection. Provider payloads and coverage vary; inspect per-entry provenance when present.", "properties": { "status": { "type": "string", "example": "success" }, "result": { "type": "string", "example": "success" }, "stats": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchTrackStat" } } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] }, "ResearchTrackStat": { "type": "object", "description": "Per-source current statistics for a single track.", "properties": { "source": { "type": "string", "description": "Requested provider platform (for example spotify or apple_music); coverage depends on provider entitlement." }, "data": { "type": "object", "description": "Provider-specific track counters. A Spotify store streams_total is a platform-displayed play count, not a royalty-bearing stream count.", "additionalProperties": true, "example": { "streams_total": 84213771 } }, "data_source": { "type": "string", "description": "Opaque provenance label identifying the measurement origin. Preserve this value for traceability. Labels can vary with the data connection and are not a supported vendor selector. Missing provenance remains unknown." }, "captured_at": { "type": "string", "format": "date-time", "description": "When this entry's values were captured into the measurement store." } }, "additionalProperties": true } } } } ``` # Track measurements Source: https://recoupable.dev/docs/api-reference/research/measurements Track measurements ## GET /api/research/tracks/{id}/measurements Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/tracks/{id}/measurements": { "get": { "summary": "Track measurements", "description": "Time-series of a track's measured counts.", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Provider-neutral track id — ISRC or Spotify track id." }, { "name": "platform", "in": "query", "schema": { "type": "string", "default": "spotify" }, "description": "Platform to read. Currently `spotify`." }, { "name": "metric", "in": "query", "schema": { "type": "string", "default": "platform_displayed_play_count" }, "description": "Metric to read." }, { "name": "from", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Inclusive start date (ISO) for the series." }, { "name": "to", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Inclusive end date (ISO) for the series." }, { "name": "granularity", "in": "query", "schema": { "type": "string", "enum": [ "daily" ], "default": "daily" }, "description": "Series granularity. `daily` returns the per-day cumulative series." }, { "name": "aggregate", "in": "query", "schema": { "type": "string", "enum": [ "run_rate" ] }, "description": "Return a derived aggregate instead of the raw series. `run_rate` returns the trailing-window annualized run-rate (a projection of the series)." }, { "name": "window", "in": "query", "schema": { "type": "string", "default": "365d" }, "description": "Trailing window for `aggregate=run_rate` (e.g. `365d`)." } ], "responses": { "200": { "description": "The measurement series (or the requested aggregate).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchMeasurementsResponse" } } } }, "400": { "description": "Validation error — bad identifier or query param.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Insufficient credits.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" } } } }, "404": { "description": "No measurements for this track yet — create a `current` measurement job to capture it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } } } } } }, "components": { "schemas": { "ResearchMeasurementsResponse": { "type": "object", "description": "A track's measured series, or — when `aggregate` is set — the derived projection.", "properties": { "status": { "type": "string", "example": "success" }, "id": { "type": "string", "description": "The provider-neutral track id queried.", "example": "USQY51771120" }, "platform": { "type": "string", "example": "spotify" }, "metric": { "type": "string", "example": "platform_displayed_play_count" }, "series": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchMeasurementPoint" }, "description": "Present unless `aggregate` is set." }, "aggregate": { "type": "object", "nullable": true, "description": "Present when `aggregate=run_rate`.", "properties": { "kind": { "type": "string", "example": "run_rate" }, "window_days": { "type": "integer", "example": 365 }, "delta": { "type": "number", "example": 42000000 }, "run_rate_annualized": { "type": "number", "example": 42000000 } } } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] }, "ResearchMeasurementPoint": { "type": "object", "properties": { "date": { "type": "string", "format": "date", "example": "2026-06-12" }, "value": { "type": "number", "description": "Cumulative metric value as of `date`.", "example": 297289495 }, "data_source": { "type": "string" } } } } } } ``` # Album measurements Source: https://recoupable.dev/docs/api-reference/research/album-measurements Album measurements ## GET /api/research/albums/{id}/measurements Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/albums/{id}/measurements": { "get": { "summary": "Album measurements", "description": "Latest measured count per track on an album, from the measurement store. `{id}` is a Spotify album id.", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Spotify album id." }, { "name": "platform", "in": "query", "schema": { "type": "string", "default": "spotify" }, "description": "Platform to read. Currently `spotify`." }, { "name": "metric", "in": "query", "schema": { "type": "string", "default": "platform_displayed_play_count" }, "description": "Metric to read." }, { "name": "latest", "in": "query", "schema": { "type": "boolean", "default": true }, "description": "Return the latest capture per track (the only supported mode today)." } ], "responses": { "200": { "description": "Latest per-track measured counts for the album.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchAlbumMeasurementsResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Insufficient credits.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" } } } }, "404": { "description": "No capture exists for this album yet — create a `current` measurement-job.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } } } } } }, "components": { "schemas": { "ResearchAlbumMeasurementsResponse": { "type": "object", "properties": { "status": { "type": "string", "example": "success" }, "id": { "type": "string", "description": "Spotify album id.", "example": "70Zkfb99ladZ3q0JVg97co" }, "platform": { "type": "string", "example": "spotify" }, "metric": { "type": "string", "example": "platform_displayed_play_count" }, "measurements": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchAlbumMeasurementItem" } } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] }, "ResearchAlbumMeasurementItem": { "type": "object", "properties": { "isrc": { "type": "string", "example": "USQY51771120" }, "spotify_track_id": { "type": "string", "nullable": true }, "name": { "type": "string", "nullable": true }, "value": { "type": "number", "description": "Latest measured count.", "example": 297289495 }, "captured_at": { "type": "string", "format": "date-time" }, "data_source": { "type": "string" } } } } } } ``` # Create measurement job Source: https://recoupable.dev/docs/api-reference/research/measurement-jobs Create a measurement job ## POST /api/research/measurement-jobs Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/measurement-jobs": { "post": { "summary": "Create a measurement job", "description": "Create a current capture or enqueue historical the configured research data connection backfill. Provide exactly one of catalog_id, album_ids or isrcs in scope; platforms supports spotify only. source=current uses the snapshot pipeline and returns an ID that can be passed to [Create catalog](/docs/api-reference/songs/catalogs-create). source=historical requires a card on file, resolves scope recordings and returns id:null with enqueued/skipped counts; it attempts to start the existing budget-gated backfill workflow, with daily maintenance as a backstop. Enqueue success is not capture completion or evidence of complete historical coverage. Songs with any historical research measurement are skipped, including potentially partial history. Historical enqueue itself makes no credit deduction; current capture estimates and provider/account gates remain separate.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchMeasurementJobRequest" } } } }, "responses": { "202": { "description": "Job accepted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchMeasurementJobResponse" } } } }, "400": { "description": "Invalid scope/source/platform, or no recordings resolvable from the selected historical scope.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Historical backfill requires a saved payment method. Unlike the ordinary research credit gate, this gate creates a card-setup Checkout Session per denied request and returns checkoutUrl; do not repeatedly submit to poll this error.", "content": { "application/json": { "schema": { "type": "object", "required": [ "status", "error", "checkoutUrl" ], "properties": { "status": { "type": "string", "enum": [ "error" ] }, "error": { "type": "string" }, "checkoutUrl": { "type": [ "string", "null" ], "description": "Card-setup checkout URL returned by Stripe. Distinct from the static billingUrl used by ordinary research credit errors." } } } } } }, "429": { "description": "Per-organization monthly cap reached.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "500": { "description": "Internal, enqueue or provider configuration error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } } } } } }, "components": { "schemas": { "ResearchMeasurementJobRequest": { "type": "object", "required": [ "scope", "source" ], "properties": { "scope": { "$ref": "#/components/schemas/ResearchMeasurementJobScope" }, "source": { "type": "string", "enum": [ "current", "historical" ], "description": "current captures present counts through the snapshot pipeline; historical enqueues the configured research data connection backfill. Historical requires a saved payment method and does not promise complete history." }, "platforms": { "type": "array", "items": { "type": "string", "enum": [ "spotify" ] }, "default": [ "spotify" ], "example": [ "spotify" ] } }, "example": { "scope": { "isrcs": [ "USQY51771120" ] }, "source": "historical", "platforms": [ "spotify" ] } }, "ResearchMeasurementJobResponse": { "oneOf": [ { "type": "object", "required": [ "status", "source", "id", "state", "album_count", "estimated_cost_usd" ], "properties": { "status": { "type": "string", "enum": [ "success" ] }, "source": { "type": "string", "enum": [ "current" ] }, "id": { "type": "string", "format": "uuid", "description": "Snapshot ID for current captures." }, "state": { "type": "string", "description": "State returned by the snapshot pipeline; reused captures need not be queued." }, "album_count": { "type": "integer" }, "estimated_cost_usd": { "type": "number" }, "reused": { "type": "boolean", "description": "Present when an existing capture is handed back." } } }, { "type": "object", "required": [ "status", "source", "id", "enqueued", "skipped" ], "properties": { "status": { "type": "string", "enum": [ "success" ] }, "source": { "type": "string", "enum": [ "historical" ] }, "id": { "type": "null", "description": "No pollable snapshot ID is created for historical backfill." }, "enqueued": { "type": "integer", "description": "Recordings submitted to the backfill queue; this does not mean history is captured." }, "skipped": { "type": "integer", "description": "Recordings with any existing historical research measurement; skipping does not prove complete history." } } } ], "description": "Current captures return a snapshot ID; historical backfill returns queue counts and id:null." }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchMeasurementJobScope": { "type": "object", "description": "Provide exactly one of `catalog_id`, `album_ids`, or `isrcs`.", "properties": { "catalog_id": { "type": "string", "format": "uuid" }, "album_ids": { "type": "array", "items": { "type": "string" } }, "isrcs": { "type": "array", "items": { "type": "string" } } } } } } } ``` # Playlist Placements Source: https://recoupable.dev/docs/api-reference/research/playlists Read available playlist placements for an artist. `status=current` selects current scope; `status=past` selects all-time scope, which can include current placements. `limit` is an optional raw string with no Recoup default; supported limits and coverage depend on the data connection. ## GET /api/research/playlists Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/playlists": { "get": { "description": "Read available playlist placements for an artist. `status=current` selects current scope; `status=past` selects all-time scope, which can include current placements. `limit` is an optional raw string with no Recoup default; supported limits and coverage depend on the data connection.\n\nUse your Recoup API key (`x-api-key`) or supported bearer credential. Successful artist and track research reads cost $0.05 (50,000 micro-dollar credits). Recoup manages the data connections. Coverage and rate limits vary; see [Research availability](/docs/research-availability).", "parameters": [ { "name": "artist", "in": "query", "required": false, "description": "Artist name; Recoup resolves the first provider search match. Required unless id is supplied. Recoup artist UUID resolution is not implemented here.", "schema": { "type": "string" } }, { "name": "platform", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "spotify", "applemusic", "deezer", "amazon", "youtube" ], "default": "spotify" } }, { "name": "status", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "current", "past" ], "default": "current" }, "description": "current uses current placements; past uses all-time scope total, which may include current placements." }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Optional raw string; no Recoup default. For similar artists, only a positive integer limits the returned list; for playlists it passes through to the provider." }, { "name": "id", "in": "query", "required": false, "description": "Research artist ID returned by search or lookup; treat it as an opaque string. Required unless artist is supplied; takes precedence when both are provided. This is not a Recoup artist/account ID.", "schema": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", "example": "artist_123" } } ], "responses": { "200": { "description": "Playlist placements", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchPlaylistsResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Insufficient research credits. The response includes a static billingUrl; no checkout session is created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" } } } }, "403": { "description": "Credential, account or provider permission denied.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "404": { "description": "Artist/track resolution or requested provider data was not found. A failed name search may be reported as 404; retry with an exact provider ID when known.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "429": { "description": "Provider rate or quota limit. The adapter returns an error status but does not forward provider Retry-After headers. Avoid an unbounded retry loop.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "500": { "description": "Missing provider configuration, transport failure or internal error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" }, "502": { "description": "Upstream provider failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "504": { "description": "the configured research data connection request timed out.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } } } } } }, "components": { "schemas": { "ResearchPlaylistsResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "example": "success" }, "placements": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchPlaylistPlacement" } } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] }, "ResearchPlaylistPlacement": { "type": "object", "description": "A playlist the artist currently appears on.", "properties": { "playlist_id": { "type": "string" }, "playlist_name": { "type": "string" }, "external_url": { "type": "string", "description": "Link to the playlist on the platform." }, "followers_total": { "type": "string", "description": "Human-readable follower count, e.g. \"34.3M\"." }, "image_url": { "type": "string", "nullable": true } }, "additionalProperties": true } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # Track Playlists Source: https://recoupable.dev/docs/api-reference/research/track-playlists Read available track playlist and activity entries. Supply a selected track `id`, or a track name `q` with optional `artist`. Name resolution selects the first Spotify match and resolves its research ID; `id` takes precedence. Filtering and pagination support depend on the data connection. With no filter flags, editorial, indie, majorCurator and popularIndie default to true. Track-name resolution can add separately charged lookup calls; reuse a selected ID to avoid them. ## GET /api/research/track/playlists Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/track/playlists": { "get": { "description": "Read available track playlist and activity entries. Supply a selected track `id`, or a track name `q` with optional `artist`. Name resolution selects the first Spotify match and resolves its research ID; `id` takes precedence. Filtering and pagination support depend on the data connection. With no filter flags, editorial, indie, majorCurator and popularIndie default to true. Track-name resolution can add separately charged lookup calls; reuse a selected ID to avoid them.\n\nUse your Recoup API key (`x-api-key`) or supported bearer credential. Successful artist and track research reads cost $0.05 (50,000 micro-dollar credits). Recoup manages the data connections. Coverage and rate limits vary; see [Research availability](/docs/research-availability).", "parameters": [ { "name": "id", "in": "query", "description": "Exact provider track ID; required unless q is supplied. Takes precedence over q. Recoup does not validate its format here.", "schema": { "type": "string", "example": "track_123" } }, { "name": "q", "in": "query", "description": "Track name to look up. Required if `id` is not provided. Combine with `artist` to narrow the match.", "schema": { "type": "string" } }, { "name": "artist", "in": "query", "description": "Optional artist name to narrow Spotify track-name matching. Ignored when id is supplied.", "schema": { "type": "string" }, "required": false }, { "name": "platform", "in": "query", "description": "Streaming platform to return playlists for. Defaults to `spotify`.", "schema": { "type": "string", "enum": [ "spotify", "applemusic", "deezer", "amazon" ], "default": "spotify" } }, { "name": "status", "in": "query", "description": "Return current or past playlist placements. Defaults to `current`.", "schema": { "type": "string", "enum": [ "current", "past" ], "default": "current" } }, { "name": "limit", "in": "query", "description": "Maximum number of placements to return. Forwarded unchanged as a raw string; Recoup does not validate its range, date format or boolean value. Provider support applies.", "schema": { "type": "string" } }, { "name": "offset", "in": "query", "description": "Pagination offset. Forwarded unchanged as a raw string; Recoup does not validate its range, date format or boolean value. Provider support applies.", "schema": { "type": "string" } }, { "name": "since", "in": "query", "description": "ISO date lower bound for placements. Forwarded unchanged as a raw string; Recoup does not validate its range, date format or boolean value. Provider support applies.", "schema": { "type": "string" } }, { "name": "until", "in": "query", "description": "ISO date upper bound for placements. Forwarded unchanged as a raw string; Recoup does not validate its range, date format or boolean value. Provider support applies.", "schema": { "type": "string" } }, { "name": "sort", "in": "query", "description": "Sort column. Forwarded unchanged as a raw string; Recoup does not validate its range, date format or boolean value. Provider support applies.", "schema": { "type": "string" } }, { "name": "editorial", "in": "query", "description": "Include editorial playlists. When no filter flags are set, defaults to `true` along with `indie`, `majorCurator`, `popularIndie`. Forwarded unchanged as a raw string; Recoup does not validate its range, date format or boolean value. Provider support applies.", "schema": { "type": "string" } }, { "name": "indie", "in": "query", "description": "Include indie playlists. Forwarded unchanged as a raw string; Recoup does not validate its range, date format or boolean value. Provider support applies.", "schema": { "type": "string" } }, { "name": "majorCurator", "in": "query", "description": "Include major-curator playlists. Forwarded unchanged as a raw string; Recoup does not validate its range, date format or boolean value. Provider support applies.", "schema": { "type": "string" } }, { "name": "popularIndie", "in": "query", "description": "Include popular indie playlists. Forwarded unchanged as a raw string; Recoup does not validate its range, date format or boolean value. Provider support applies.", "schema": { "type": "string" } }, { "name": "personalized", "in": "query", "description": "Include personalized playlists. Forwarded unchanged as a raw string; Recoup does not validate its range, date format or boolean value. Provider support applies.", "schema": { "type": "string" } }, { "name": "chart", "in": "query", "description": "Include chart playlists. Forwarded unchanged as a raw string; Recoup does not validate its range, date format or boolean value. Provider support applies.", "schema": { "type": "string" } }, { "name": "newMusicFriday", "in": "query", "description": "Include New Music Friday playlists. Forwarded unchanged as a raw string; Recoup does not validate its range, date format or boolean value. Provider support applies.", "schema": { "type": "string" } }, { "name": "thisIs", "in": "query", "description": "Include \"This Is\" artist playlists. Forwarded unchanged as a raw string; Recoup does not validate its range, date format or boolean value. Provider support applies.", "schema": { "type": "string" } }, { "name": "radio", "in": "query", "description": "Include algorithmic radio playlists. Forwarded unchanged as a raw string; Recoup does not validate its range, date format or boolean value. Provider support applies.", "schema": { "type": "string" } }, { "name": "brand", "in": "query", "description": "Include brand playlists. Forwarded unchanged as a raw string; Recoup does not validate its range, date format or boolean value. Provider support applies.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Playlist placements for the track.", "content": { "application/json": { "schema": { "type": "object", "required": [ "status", "placements" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "example": "success" }, "placements": { "type": "array", "description": "Playlist placement objects returned by the configured provider (shape varies per platform).", "items": { "type": "object" } } } } } } }, "400": { "description": "Validation error — missing `id`/`q`, invalid `platform`, or invalid `status`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "402": { "description": "Insufficient research credits. The response includes a static billingUrl; no checkout session is created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" } } } }, "403": { "description": "Credential, account or provider permission denied.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "404": { "description": "Artist/track resolution or requested provider data was not found. A failed name search may be reported as 404; retry with an exact provider ID when known.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "429": { "description": "Provider rate or quota limit. The adapter returns an error status but does not forward provider Retry-After headers. Avoid an unbounded retry loop.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "500": { "description": "Missing provider configuration, transport failure or internal error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" }, "502": { "description": "Upstream provider failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "504": { "description": "the configured research data connection request timed out.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } } } } } }, "components": { "schemas": { "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # Web Search Source: https://recoupable.dev/docs/api-reference/research/web Search the web for real-time information. Returns ranked results with titles, URLs, and content snippets. Use for narrative context, press coverage, and cultural research that structured data endpoints don't cover. ## POST /api/research/web Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/web": { "post": { "description": "Search the web for real-time information. Returns ranked results with titles, URLs, and content snippets. Use for narrative context, press coverage, and cultural research that structured data endpoints don't cover.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchWebRequest" } } } }, "responses": { "200": { "description": "Web search results", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchWebResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" } } } } }, "components": { "schemas": { "ResearchWebRequest": { "type": "object", "required": [ "query" ], "description": "Request body for web research.", "properties": { "query": { "type": "string", "description": "The search query — what you want to find on the web." }, "max_results": { "type": "integer", "minimum": 1, "maximum": 20, "default": 10, "description": "Maximum number of results to return." }, "country": { "type": "string", "minLength": 2, "maxLength": 2, "description": "ISO country code for regional results (e.g., 'US', 'GB')." } } }, "ResearchWebResponse": { "type": "object", "description": "Web search results with titles, URLs, and content snippets.", "properties": { "status": { "type": "string", "example": "success" }, "results": { "type": "array", "description": "Ranked web search results.", "items": { "type": "object", "properties": { "title": { "type": "string" }, "url": { "type": "string" }, "snippet": { "type": "string", "description": "Content snippet from the search result." }, "date": { "type": "string", "nullable": true, "description": "Publication date if available." }, "last_updated": { "type": "string", "nullable": true, "description": "Last updated date if available." } } } }, "formatted": { "type": "string", "description": "Results formatted as markdown for easy reading." } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # Deep Research Source: https://recoupable.dev/docs/api-reference/research/deep Perform deep, comprehensive research on a topic. Browses multiple sources extensively and returns a cited report. Use for full artist deep dives, competitive analysis, and any research requiring synthesis across many sources. ## POST /api/research/deep Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/deep": { "post": { "description": "Perform deep, comprehensive research on a topic. Browses multiple sources extensively and returns a cited report. Use for full artist deep dives, competitive analysis, and any research requiring synthesis across many sources.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchDeepRequest" } } } }, "responses": { "200": { "description": "Deep research report with citations", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchDeepResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" } } } } }, "components": { "schemas": { "ResearchDeepRequest": { "type": "object", "required": [ "query" ], "description": "Request body for deep research. Performs comprehensive multi-source analysis.", "properties": { "query": { "type": "string", "description": "The research question — be specific and detailed for best results." } } }, "ResearchDeepResponse": { "type": "object", "description": "Comprehensive research report with citations.", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "example": "success" }, "content": { "type": "string", "description": "The full research report as markdown." }, "citations": { "type": "array", "description": "Source URLs cited in the report.", "items": { "type": "string", "format": "uri" } } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # People Search Source: https://recoupable.dev/docs/api-reference/research/people Search for people in the music industry — artists, managers, A&R reps, producers. Returns multi-source profiles including LinkedIn data. ## POST /api/research/people Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/people": { "post": { "description": "Search for people in the music industry — artists, managers, A&R reps, producers. Returns multi-source profiles including LinkedIn data.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchPeopleRequest" } } } }, "responses": { "200": { "description": "People search results", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchPeopleResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" } } } } }, "components": { "schemas": { "ResearchPeopleRequest": { "type": "object", "required": [ "query" ], "properties": { "query": { "type": "string", "description": "Natural language search for people (e.g., \"A&R reps at Atlantic Records\")." }, "num_results": { "type": "integer", "minimum": 1, "maximum": 100, "default": 10, "description": "Number of results to return." } } }, "ResearchPeopleResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "example": "success" }, "results": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchPeopleResult" } } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchPeopleResult": { "type": "object", "properties": { "title": { "type": "string", "description": "Person name and role." }, "url": { "type": "string", "format": "uri", "description": "Profile URL (often LinkedIn)." }, "id": { "type": "string", "description": "Unique result identifier from the search provider." }, "publishedDate": { "type": "string", "nullable": true, "description": "Date the profile or page was published." }, "author": { "type": "string", "nullable": true, "description": "Author of the page, if available." }, "highlights": { "type": "array", "items": { "type": "string" }, "nullable": true, "description": "Key excerpts from the profile." }, "summary": { "type": "string", "nullable": true, "description": "Brief summary of the person." } } } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # Extract URL Source: https://recoupable.dev/docs/api-reference/research/extract Extract clean markdown content from one or more public URLs. Handles JavaScript-heavy pages and PDFs. Returns focused excerpts aligned to an objective, or full page content. ## POST /api/research/extract Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/extract": { "post": { "description": "Extract clean markdown content from one or more public URLs. Handles JavaScript-heavy pages and PDFs. Returns focused excerpts aligned to an objective, or full page content.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchExtractRequest" } } } }, "responses": { "200": { "description": "Extracted content from URLs", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchExtractResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" } } } } }, "components": { "schemas": { "ResearchExtractRequest": { "type": "object", "required": [ "urls" ], "properties": { "urls": { "type": "array", "items": { "type": "string", "format": "uri" }, "maxItems": 10, "description": "URLs to extract content from (max 10).", "minItems": 1 }, "objective": { "type": "string", "description": "What information to focus on (optional, max 3000 chars)." }, "full_content": { "type": "boolean", "default": false, "description": "Return full page content instead of focused excerpts." } } }, "ResearchExtractResponse": { "type": "object", "required": [ "status", "results" ], "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "example": "success" }, "results": { "type": "array", "items": { "$ref": "#/components/schemas/ResearchExtractResult" } }, "errors": { "type": "array", "items": { "type": "object" }, "description": "URLs that failed to extract. Only present when one or more URLs could not be processed." } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } }, "ResearchExtractResult": { "type": "object", "properties": { "url": { "type": "string", "format": "uri" }, "title": { "type": "string", "nullable": true }, "publish_date": { "type": "string", "nullable": true, "description": "Publish date in YYYY-MM-DD format." }, "excerpts": { "type": "array", "items": { "type": "string" }, "nullable": true, "description": "Focused excerpts as markdown." }, "full_content": { "type": "string", "nullable": true, "description": "Full page content as markdown." } } } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # Enrich Source: https://recoupable.dev/docs/api-reference/research/enrich Enrich an entity with structured data from web research. Provide a description of who or what to research and a JSON schema defining the fields to extract. Returns typed data with citations. **Important:** The `schema` object must include `"type": "object"` at the top level — requests without an explicit type will be rejected. ## POST /api/research/enrich Full OpenAPI specification: https://recoupable.dev/docs/spec/research.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Research", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/research/enrich": { "post": { "description": "Enrich an entity with structured data from web research. Provide a description of who or what to research and a JSON schema defining the fields to extract. Returns typed data with citations. **Important:** The `schema` object must include `\"type\": \"object\"` at the top level — requests without an explicit type will be rejected.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchEnrichRequest" } } } }, "responses": { "200": { "description": "Structured enrichment data", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchEnrichResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "401": { "description": "Authentication failed — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" } } } }, "501": { "$ref": "#/components/responses/ResearchDataSourceUnsupported" } } } } }, "components": { "schemas": { "ResearchEnrichRequest": { "type": "object", "required": [ "input", "schema" ], "properties": { "input": { "type": "string", "description": "What to research (e.g., \"Drake rapper from Dallas Texas\")." }, "schema": { "type": "object", "description": "JSON schema defining the fields to extract. Must include `\"type\": \"object\"` at the top level.", "additionalProperties": true, "properties": { "type": { "type": "string", "enum": [ "object" ], "description": "Must be \"object\"" }, "properties": { "type": "object", "description": "Field definitions to extract", "additionalProperties": true } }, "required": [ "type", "properties" ] }, "processor": { "type": "string", "enum": [ "base", "core", "ultra" ], "default": "base", "description": "Research depth: base (fast), core (balanced), ultra (comprehensive)." } } }, "ResearchEnrichResponse": { "type": "object", "properties": { "status": { "type": "string", "example": "success" }, "output": { "type": "object", "description": "Structured data matching the provided schema.", "additionalProperties": true }, "citations": { "type": "array", "description": "Source citations supporting the enriched output.", "items": { "type": "object", "properties": { "url": { "type": "string", "format": "uri" }, "title": { "type": "string" }, "field": { "type": "string", "description": "Which output field this citation supports." } } } } } }, "ResearchErrorResponse": { "type": "object", "required": [ "status", "error" ], "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).", "properties": { "status": { "type": "string", "enum": [ "error" ], "example": "error" }, "error": { "type": "string", "description": "Human-readable error message describing what went wrong.", "example": "Missing required parameter: artist" } } } }, "responses": { "ResearchDataSourceUnsupported": { "description": "The configured research data source does not support this endpoint or data shape.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResearchErrorResponse" }, "example": { "status": "error", "error": "Request failed with status 501" } } } } } } } ``` # Get Songs Source: https://recoupable.dev/docs/api-reference/songs/songs Retrieve songs from the database with optional filtering by ISRC (International Standard Recording Code) or artist account. This endpoint joins the songs table with song_artists and accounts tables to provide comprehensive song information. > **Note** > The Songs API also supports POST requests for bulk create/fetch operations. See the [Create Songs](https://recoupable.dev/docs/api-reference/songs/create) endpoint. ## GET /api/songs Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/songs": { "get": { "description": "Retrieve songs from the database with optional filtering by ISRC (International Standard Recording Code) or artist account. This endpoint joins the songs table with song_artists and accounts tables to provide comprehensive song information.", "parameters": [ { "name": "isrc", "in": "query", "description": "International Standard Recording Code to filter by specific song", "required": false, "schema": { "type": "string" } }, { "name": "artist_account_id", "in": "query", "description": "Artist account ID to filter songs by artist", "required": false, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Songs retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SongsResponse" } } } }, "400": { "description": "Bad request - invalid parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SongsErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SongsErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SongsErrorResponse" } } } } } } } }, "components": { "schemas": { "SongsResponse": { "type": "object", "description": "Response containing songs data", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "songs": { "type": "array", "items": { "$ref": "#/components/schemas/Song" }, "description": "Array of song objects with artist information" }, "error": { "type": "string", "description": "Error message (only present if status is 'error')" } } }, "SongsErrorResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "Song": { "type": "object", "description": "A song with its metadata and associated artists", "properties": { "isrc": { "type": "string", "description": "International Standard Recording Code (primary key)" }, "name": { "type": "string", "description": "Name of the song" }, "album": { "type": "string", "description": "Name of the album the song belongs to" }, "notes": { "type": "string", "description": "Notes for the song" }, "updated_at": { "type": "string", "format": "date-time", "description": "ISO timestamp of when the song data was last updated" }, "artists": { "type": "array", "items": { "$ref": "#/components/schemas/SongArtist" }, "description": "Array of artist objects associated with this song" } } }, "SongArtist": { "type": "object", "description": "Artist associated with a song", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the artist account" }, "name": { "type": "string", "nullable": true, "description": "Name of the artist (can be null)" }, "timestamp": { "type": "integer", "nullable": true, "description": "Timestamp associated with the artist account (can be null)" } } } } } } ``` # Create Songs Source: https://recoupable.dev/docs/api-reference/songs/create **Availability: pending reconciliation.** This song-creation REST route has no matching route in the inspected API source. Resolve the supported creation contract separately; existing catalog or analysis operations do not establish this write route. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation. > **Note** > This endpoint performs bulk create/fetch operations. For each song, the API attempts to look up metadata via internal search. If no data is found, optional fallback fields (name, album, notes, artists) are used. ## POST /api/songs Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/songs": { "post": { "description": "**Availability: pending reconciliation.** This song-creation REST route has no matching route in the inspected API source. Resolve the supported creation contract separately; existing catalog or analysis operations do not establish this write route. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation.\n\nBulk create or fetch songs by ISRC. For each song, the API attempts to look up metadata via internal search. If no data is found, optional fallback fields (name, album, notes, artists) are used.", "requestBody": { "description": "Array of songs to create or fetch", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSongsRequest" } } } }, "responses": { "200": { "description": "Songs created or fetched successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SongsResponse" } } } }, "400": { "description": "Bad request - invalid parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SongsErrorResponse" } } } } }, "x-recoup-availability": "pending" } } }, "components": { "schemas": { "CreateSongsRequest": { "type": "object", "required": [ "songs" ], "properties": { "songs": { "type": "array", "items": { "$ref": "#/components/schemas/CreateSongInput" }, "description": "Array of song inputs for bulk create/fetch" } } }, "SongsResponse": { "type": "object", "description": "Response containing songs data", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "songs": { "type": "array", "items": { "$ref": "#/components/schemas/Song" }, "description": "Array of song objects with artist information" }, "error": { "type": "string", "description": "Error message (only present if status is 'error')" } } }, "SongsErrorResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "CreateSongInput": { "type": "object", "required": [ "isrc" ], "properties": { "isrc": { "type": "string", "description": "International Standard Recording Code of the song to create or fetch" }, "name": { "type": "string", "description": "Optional. Song name, applied only if internal search cannot find valid info" }, "album": { "type": "string", "description": "Optional. Album name, applied only if internal search cannot find valid info" }, "notes": { "type": "string", "description": "Optional. Notes for the song, applied only if internal search cannot find valid info" }, "artists": { "type": "array", "items": { "type": "string" }, "description": "Optional array of artist names, applied only if internal search cannot find valid info" } } }, "Song": { "type": "object", "description": "A song with its metadata and associated artists", "properties": { "isrc": { "type": "string", "description": "International Standard Recording Code (primary key)" }, "name": { "type": "string", "description": "Name of the song" }, "album": { "type": "string", "description": "Name of the album the song belongs to" }, "notes": { "type": "string", "description": "Notes for the song" }, "updated_at": { "type": "string", "format": "date-time", "description": "ISO timestamp of when the song data was last updated" }, "artists": { "type": "array", "items": { "$ref": "#/components/schemas/SongArtist" }, "description": "Array of artist objects associated with this song" } } }, "SongArtist": { "type": "object", "description": "Artist associated with a song", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the artist account" }, "name": { "type": "string", "nullable": true, "description": "Name of the artist (can be null)" }, "timestamp": { "type": "integer", "nullable": true, "description": "Timestamp associated with the artist account (can be null)" } } } } } } ``` # Analyze Songs Source: https://recoupable.dev/docs/api-reference/songs/analyze Analyze music using a state-of-the-art Audio Language Model ([Music Flamingo](https://www.universalmusic.com/universal-music-group-to-transform-music-experience-for-billions-of-fans-with-nvidia-ai/)) that listens directly to the audio waveform. ## POST /api/songs/analyze Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/songs/analyze": { "post": { "description": "Analyze music using a state-of-the-art Audio Language Model ([Music Flamingo](https://www.universalmusic.com/universal-music-group-to-transform-music-experience-for-billions-of-fans-with-nvidia-ai/)) that listens directly to the audio waveform.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Music analysis request", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SongAnalyzeRequest" } } } }, "responses": { "200": { "description": "Music analysis completed successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SongAnalyzeResponse" } } } }, "400": { "description": "Bad request — missing or invalid fields, including a missing `audio_url` (`missing_fields: [\"audio_url\"]`) or both `preset` and `prompt` in one request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SongAnalyzeErrorResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key / Bearer token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SongAnalyzeErrorResponse" } } } }, "402": { "description": "Insufficient credits — the balance cannot cover the base price of this request (`$0.05` per model call, `$0.65` for `full_report`). The model is not called, nothing is charged, and no Stripe object is created. The body includes a static `billingUrl`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SongAnalyzeInsufficientCreditsResponse" } } } }, "422": { "description": "Unverifiable audio. Before the model is called, `audio_url` is checked with a `HEAD` request (a ranged `GET` for hosts that reject `HEAD`). It must answer 2xx within 10 seconds with an `audio/*` or `application/octet-stream` content type. When it does not, the model is not called and nothing is charged.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SongAnalyzeAudioUrlErrorResponse" } } } }, "500": { "description": "Server error — upstream model unavailable or inference failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SongAnalyzeErrorResponse" } } } } } } } }, "components": { "schemas": { "SongAnalyzeRequest": { "type": "object", "description": "Provide exactly one of `preset` or `prompt`, plus `audio_url`. Use `preset` for structured analysis workflows, or `prompt` for free-form questions about the audio.", "required": [ "audio_url" ], "properties": { "preset": { "type": "string", "enum": [ "catalog_metadata", "mood_tags", "lyric_transcription", "mix_feedback", "song_description", "music_theory", "similar_artists", "sample_detection", "sync_brief_match", "audience_profile", "content_advisory", "playlist_pitch", "artist_development_notes", "full_report" ], "description": "Name of a curated analysis preset. Use instead of prompt for structured, optimized output. The 'full_report' preset runs all 13 presets in parallel and returns a comprehensive report. See [List Analyze Presets](/docs/api-reference/songs/analyze-presets) for the full list of available presets.", "example": "catalog_metadata" }, "prompt": { "type": "string", "minLength": 1, "maxLength": 24000, "description": "Text prompt or question about the music", "example": "Describe the genre, tempo, and mood of this track." }, "audio_url": { "type": "string", "format": "uri", "description": "Public URL to an audio file (MP3, WAV, or FLAC, up to 20 minutes). Required in every mode, including a custom `prompt`. Verified before the model is called: the URL must answer a `HEAD` request (or a ranged `GET`) with 2xx within 10 seconds and an `audio/*` or `application/octet-stream` content type, else the request returns 422 and nothing is charged.", "example": "https://example.com/song.mp3" }, "max_new_tokens": { "type": "integer", "minimum": 1, "maximum": 2048, "default": 512, "description": "Maximum number of tokens to generate", "example": 512 }, "temperature": { "type": "number", "minimum": 0, "maximum": 2, "default": 1, "description": "Controls output creativity — higher values produce more varied responses", "example": 0.7 }, "top_p": { "type": "number", "minimum": 0, "maximum": 1, "default": 1, "description": "Nucleus sampling probability cutoff", "example": 0.9 }, "do_sample": { "type": "boolean", "default": false, "description": "Enable sampling (set true when using temperature or top_p)", "example": false } } }, "SongAnalyzeResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Request status" }, "preset": { "type": "string", "description": "Preset used for analysis, when applicable", "example": "catalog_metadata" }, "response": { "description": "Model output for single-preset or custom-prompt analysis. May be plain text or structured JSON depending on the preset." }, "report": { "type": "object", "description": "Full report payload returned only when using the `full_report` preset" }, "elapsed_seconds": { "type": "number", "format": "float", "description": "Inference time in seconds" } } }, "SongAnalyzeErrorResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Error status" }, "missing_fields": { "type": "array", "description": "Path to the first invalid or missing field when validation fails", "items": { "type": "string" } }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "SongAnalyzeInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks credits for the analysis. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "description": "The account's balance, in credits (micro-dollars: 1,000,000 = `$1.00`).", "example": 0 }, "required_credits": { "type": "integer", "description": "The base price of this request, in credits: 50000 (`$0.05`) for a single preset or custom prompt, 650000 (`$0.65`) for `full_report`. The per-second inference charge is added after the model returns.", "example": 50000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] }, "SongAnalyzeAudioUrlErrorResponse": { "type": "object", "description": "Returned (422) when `audio_url` could not be verified. The model is not called and nothing is charged.", "properties": { "status": { "type": "string", "enum": [ "error" ] }, "error": { "type": "string", "enum": [ "audio_url_unreachable", "audio_url_not_audio" ], "description": "`audio_url_unreachable`: the URL did not answer 2xx within 10 seconds. `audio_url_not_audio`: it answered, but with a content type that is not `audio/*` or `application/octet-stream`.", "example": "audio_url_not_audio" }, "message": { "type": "string", "description": "What the check observed, for the caller's logs.", "example": "audio_url answered 200 with content type text/html" } }, "required": [ "status", "error", "message" ] } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # List Analyze Presets Source: https://recoupable.dev/docs/api-reference/songs/analyze-presets Lists all available music analysis presets. Each preset is a curated prompt with optimized generation parameters for a specific use case (e.g. catalog metadata enrichment, sync licensing analysis, audience profiling). Requires authentication via API key or Bearer token. ## GET /api/songs/analyze/presets Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/songs/analyze/presets": { "get": { "description": "Lists all available music analysis presets. Each preset is a curated prompt with optimized generation parameters for a specific use case (e.g. catalog metadata enrichment, sync licensing analysis, audience profiling). Requires authentication via API key or Bearer token.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Presets listed successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "success" ] }, "presets": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string", "description": "Preset identifier to pass in the analyze request" }, "label": { "type": "string", "description": "Human-readable preset name" }, "description": { "type": "string", "description": "What this preset does" }, "requiresAudio": { "type": "boolean", "description": "Always `true`: every analyze request requires `audio_url`, whichever preset or prompt it uses. Kept on the response for compatibility.", "example": true }, "responseFormat": { "type": "string", "enum": [ "json", "text" ], "description": "Expected response format" } } } } } } } } }, "401": { "description": "Unauthorized — invalid or missing API key / Bearer token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SongAnalyzeErrorResponse" } } } } } } } }, "components": { "schemas": { "SongAnalyzeErrorResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Error status" }, "missing_fields": { "type": "array", "description": "Path to the first invalid or missing field when validation fails", "items": { "type": "string" } }, "error": { "type": "string", "description": "Error message describing what went wrong" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Catalogs Source: https://recoupable.dev/docs/api-reference/songs/catalogs Retrieve the catalogs visible to a specific account. Returns the catalogs the account owns directly, plus the catalogs owned by any organization the account belongs to - so every member of an organization sees that organization's catalogs without passing the organization's id. A catalog owned both directly and through an organization is returned once. An account that belongs to no organization receives only its own catalogs. Create an organization-owned catalog with [Create catalog](/docs/api-reference/songs/catalogs-create) or [Run valuation](/docs/api-reference/songs/valuation-run). ## GET /api/accounts/{id}/catalogs Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/accounts/{id}/catalogs": { "get": { "description": "Retrieve the catalogs visible to a specific account. Returns the catalogs the account owns directly, plus the catalogs owned by any organization the account belongs to - so every member of an organization sees that organization's catalogs without passing the organization's id. A catalog owned both directly and through an organization is returned once. An account that belongs to no organization receives only its own catalogs. Create an organization-owned catalog with [Create catalog](/docs/api-reference/songs/catalogs-create) or [Run valuation](/docs/api-reference/songs/valuation-run).\n\nEach catalog carries its estimated value band and its owner, so a list of catalogs can be ranked by worth and a member can tell an organization's catalog from their own. `valuation` is null for a catalog with no measured songs; `owner.image` is null when the owner account has no avatar.", "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier of the account to query. The catalogs of every organization this account belongs to are included in the response.", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Catalogs retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogsResponse" } } } }, "400": { "description": "Bad request - missing or invalid account id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogsErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogsErrorResponse" } } } }, "403": { "description": "Forbidden - caller lacks access to the target account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogsErrorResponse" } } } }, "404": { "description": "Account not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogsErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogsErrorResponse" } } } } } } } }, "components": { "schemas": { "CatalogsResponse": { "type": "object", "description": "Response containing catalogs data", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "catalogs": { "type": "array", "items": { "$ref": "#/components/schemas/CatalogListItem" }, "description": "Array of catalog objects, each with its estimated value and its owner" }, "error": { "type": "string", "description": "Error message (only present if status is 'error')" } } }, "CatalogsErrorResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "CatalogListItem": { "type": "object", "description": "A catalog as returned by [Get catalogs](/docs/api-reference/songs/catalogs): the catalog plus its estimated value and its owner. Only this list carries those two fields - the create and rename responses return the bare catalog.", "required": [ "id", "name", "created_at", "updated_at", "measured_song_count", "valuation", "owner" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the catalog" }, "name": { "type": "string", "description": "Name of the catalog" }, "created_at": { "type": "string", "format": "date-time", "description": "ISO timestamp of when the catalog was created" }, "updated_at": { "type": "string", "format": "date-time", "description": "ISO timestamp of when the catalog was last updated" }, "measured_song_count": { "type": "integer", "description": "Songs in the catalog with at least one play-count measurement. 0 for a catalog that was never measured, in which case valuation is null." }, "valuation": { "oneOf": [ { "$ref": "#/components/schemas/ValuationBand" }, { "type": "null" } ], "description": "Estimated value band - the same schema and model as [Get catalog measurements](/docs/api-reference/songs/catalog-measurements) returns, so a list and the report it opens cannot disagree. Null when nothing in the catalog has been measured; clients should say so rather than render $0." }, "owner": { "$ref": "#/components/schemas/CatalogOwner" } } }, "ValuationBand": { "type": "object", "description": "Estimated catalog value in USD, derived at read time from the latest measurements - the same model as the recoupable.dev valuation card. Annual run-rate = lifetime streams / catalog age (lifetime-average proxy), converted to net label share (all-DSP gross-up 1.25/1.4/1.6, 15% distribution fee, 25% royalty share, $0.0035 per Spotify stream) and multiplied by a 10x/13x/16x master-catalog market multiple.", "properties": { "low": { "type": "number", "description": "Low estimate in USD (1.25x gross-up, 10x multiple)" }, "mid": { "type": "number", "description": "Central estimate in USD (1.4x gross-up, 13x multiple)" }, "high": { "type": "number", "description": "High estimate in USD (1.6x gross-up, 16x multiple)" } } }, "CatalogOwner": { "type": "object", "description": "The account a catalog belongs to. A catalog owned both directly and through an organization reports the organization - that is the ownership worth surfacing to a member.", "required": [ "id", "name", "image", "is_organization" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Owner account id" }, "name": { "type": "string", "nullable": true, "description": "Owner display name, null when the account has none" }, "image": { "type": "string", "nullable": true, "description": "Owner avatar URL from account_info, null when the account has no image. Clients should fall back to initials rather than render a broken image." }, "is_organization": { "type": "boolean", "description": "True when the owner is an organization the caller belongs to, false when it is a personal account" } } } } } } ``` # Create Catalog Source: https://recoupable.dev/docs/api-reference/songs/catalogs-create Create a catalog, owned by the calling account or - with organization_id - by one of the caller's organizations, in which case every member of that organization sees it via [Get Catalogs](/docs/api-reference/songs/catalogs). When materializing from a valuation snapshot, the endpoint also attaches the snapshot's canonical artist to the caller's roster: the measured songs' existing song-artist links are resolved to the dominant artist account, which is added to the caller's roster if not already present. The attach also runs on idempotent re-claims of an already-materialized snapshot. The roster attach always targets the calling account, even when the catalog is owned by an organization. ## POST /api/catalogs Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/catalogs": { "post": { "description": "Create a catalog, owned by the calling account or - with organization_id - by one of the caller's organizations, in which case every member of that organization sees it via [Get Catalogs](/docs/api-reference/songs/catalogs). When materializing from a valuation snapshot, the endpoint also attaches the snapshot's canonical artist to the caller's roster: the measured songs' existing song-artist links are resolved to the dominant artist account, which is added to the caller's roster if not already present. The attach also runs on idempotent re-claims of an already-materialized snapshot. The roster attach always targets the calling account, even when the catalog is owned by an organization.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Catalog to create. Provide name, snapshot, or both - at least one is required.", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateCatalogRequest" } } } }, "responses": { "200": { "description": "Catalog created, or the existing catalog returned when re-materializing the same snapshot (idempotent)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateCatalogResponse" } } } }, "400": { "description": "Bad request - neither name nor snapshot provided, or invalid snapshot", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } }, "403": { "description": "Forbidden - the snapshot belongs to a different account, or the caller is not a member of the organization named by organization_id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } }, "404": { "description": "Not found - no snapshot exists for the supplied snapshot", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } } } } } }, "components": { "schemas": { "CreateCatalogRequest": { "type": "object", "description": "Request body for creating a catalog. At least one of name or snapshot must be supplied. The catalog is owned by the authenticated account unless organization_id names an organization to own it instead.", "properties": { "name": { "type": "string", "description": "Optional. Display name for the catalog. If omitted when snapshot is supplied, a name is derived from the source run." }, "snapshot": { "type": "string", "format": "uuid", "description": "Optional. ID of a completed playcount snapshot (valuation run) owned by the authenticated account. Its measured ISRCs are added to the new catalog as catalog songs. Create one with [Create measurement job](/docs/api-reference/research/measurement-jobs)." }, "organization_id": { "type": "string", "format": "uuid", "description": "Optional. ID of an organization to own the catalog instead of the calling account. Every member of that organization then sees the catalog in their own [Get Catalogs](/docs/api-reference/songs/catalogs) response, without passing the organization's id. The caller must be a member of the organization; otherwise the request is rejected with 403. Omit to own the catalog personally, which is the default." } } }, "CreateCatalogResponse": { "type": "object", "description": "Response returned after creating, or idempotently re-fetching, a catalog", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "catalog": { "$ref": "#/components/schemas/Catalog" }, "songs_added": { "type": "integer", "description": "Number of catalog songs materialized from the source. 0 when no snapshot was supplied, or when the run was already materialized (idempotent re-claim)." }, "error": { "type": "string", "description": "Error message (only present if status is 'error')" } } }, "CatalogSongsErrorResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "Catalog": { "type": "object", "description": "A catalog with its metadata", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the catalog" }, "name": { "type": "string", "description": "Name of the catalog" }, "created_at": { "type": "string", "format": "date-time", "description": "ISO timestamp of when the catalog was created" }, "updated_at": { "type": "string", "format": "date-time", "description": "ISO timestamp of when the catalog was last updated" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Rename Catalog Source: https://recoupable.dev/docs/api-reference/songs/catalog-rename Rename a catalog. The only mutable field is the display name — catalog membership is managed through [Add catalog songs](/docs/api-reference/songs/catalog-songs-add) and [Remove catalog songs](/docs/api-reference/songs/catalog-songs-delete). ## PATCH /api/catalogs/{catalogId} Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/catalogs/{catalogId}": { "patch": { "description": "Rename a catalog. The only mutable field is the display name — catalog membership is managed through [Add catalog songs](/docs/api-reference/songs/catalog-songs-add) and [Remove catalog songs](/docs/api-reference/songs/catalog-songs-delete).\n\nCatalogs created by [Run valuation](/docs/api-reference/songs/valuation-run) are named after the measured artist. Catalogs created before that shipped are all named `Valuation Catalog` — this endpoint is how you give them real names.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "catalogId", "in": "path", "description": "ID of the catalog to rename.", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "description": "The new catalog name.", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateCatalogRequest" } } } }, "responses": { "200": { "description": "Catalog renamed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateCatalogResponse" } } } }, "400": { "description": "Bad request - catalogId is not a valid UUID, or name is missing or empty", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogsErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogsErrorResponse" } } } }, "404": { "description": "Catalog not found - no catalog with this ID is visible to the caller. A catalog that exists but belongs to neither the authenticated account nor one of its organizations returns 404, not 403: the same visibility rule the catalog read paths use, so a catalog you cannot see is indistinguishable from one that does not exist.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogsErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogsErrorResponse" } } } } } } } }, "components": { "schemas": { "UpdateCatalogRequest": { "type": "object", "description": "Request body for renaming a catalog.", "required": [ "name" ], "properties": { "name": { "type": "string", "minLength": 1, "description": "New display name for the catalog. Must not be empty." } } }, "UpdateCatalogResponse": { "type": "object", "description": "Response returned after renaming a catalog", "required": [ "status", "catalog" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "catalog": { "$ref": "#/components/schemas/Catalog" } } }, "CatalogsErrorResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "Catalog": { "type": "object", "description": "A catalog with its metadata", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the catalog" }, "name": { "type": "string", "description": "Name of the catalog" }, "created_at": { "type": "string", "format": "date-time", "description": "ISO timestamp of when the catalog was created" }, "updated_at": { "type": "string", "format": "date-time", "description": "ISO timestamp of when the catalog was last updated" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Delete Catalog Source: https://recoupable.dev/docs/api-reference/songs/catalog-delete Delete a catalog. Removes the catalog, its song membership (catalog_songs), its ownership links (account_catalogs) and its stored valuation history (catalog_valuations). ## DELETE /api/catalogs/{catalogId} Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/catalogs/{catalogId}": { "delete": { "description": "Delete a catalog. Removes the catalog, its song membership (catalog_songs), its ownership links (account_catalogs) and its stored valuation history (catalog_valuations).\n\nThe measurement that produced the catalog is **not** deleted. A playcount snapshot is a metered capture that spent credits and cannot be reproduced for a past date, while a catalog is a label over songs that can be rebuilt from it in one call — so deleting a catalog only clears the snapshot's reference to it. The snapshot then reads as unclaimed, and [Create catalog](/docs/api-reference/songs/catalogs-create) can re-materialize it under a correct name. The released snapshot IDs are returned so that recovery does not require hunting for them.\n\nUse this for genuine duplicates. To fix a catalog's name, use [Rename catalog](/docs/api-reference/songs/catalog-rename) instead — it keeps the catalog's measurements and valuation history intact.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "catalogId", "in": "path", "description": "ID of the catalog to delete.", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Catalog deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteCatalogResponse" } } } }, "400": { "description": "Bad request - catalogId is not a valid UUID", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogsErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogsErrorResponse" } } } }, "404": { "description": "Catalog not found - no catalog with this ID is visible to the caller. A catalog that exists but belongs to neither the authenticated account nor one of its organizations returns 404, not 403: the same visibility rule the catalog read paths use, so a catalog you cannot see is indistinguishable from one that does not exist.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogsErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogsErrorResponse" } } } } } } } }, "components": { "schemas": { "DeleteCatalogResponse": { "type": "object", "description": "Response returned after deleting a catalog", "required": [ "status", "catalog_id", "released_snapshot_ids" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "catalog_id": { "type": "string", "format": "uuid", "description": "ID of the deleted catalog" }, "released_snapshot_ids": { "type": "array", "description": "IDs of the playcount snapshots that pointed at this catalog. Those snapshots still exist - only their catalog reference was cleared - so each can be re-materialized into a new, correctly named catalog with Create catalog. Empty when the catalog was not created from a measurement run.", "items": { "type": "string", "format": "uuid" } } } }, "CatalogsErrorResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Run Valuation Source: https://recoupable.dev/docs/api-reference/songs/valuation-run Run valuation ## POST /api/valuation Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/valuation": { "post": { "summary": "Run valuation", "description": "Generate a catalog from a Spotify artist in one call. Resolves the artist's releases, captures current Spotify play counts (spending the account's credits), materializes a catalog from the resulting snapshot (idempotent - see [Create catalog](/docs/api-reference/songs/catalogs-create)), and returns the catalog with its estimated value band. The catalog is owned by the calling account unless organization_id names one of the caller's organizations to own it instead, in which case every member of that organization sees it. The calling account is always taken from the credentials, never the body, and it is the account whose credits are spent. The searched artist is also linked to the caller's roster (so a funnel signup lands with a populated `GET /api/artists` it can confirm); when the catalog's songs already resolve a canonical artist that one is used, otherwise the searched Spotify artist is linked directly. Synchronous: the request waits for the capture to land (typically under two minutes). Each run also persists a row in the catalog's valuation history, readable via [Get Catalog Valuations](/docs/api-reference/songs/catalog-valuations).", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "The Spotify artist to value.", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RunValuationRequest" } } } }, "responses": { "200": { "description": "Catalog created (or idempotently re-fetched) with its estimated value band.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RunValuationResponse" } } } }, "400": { "description": "Bad request - missing or invalid spotify_artist_id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } }, "402": { "description": "Insufficient credits to measure the catalog", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } }, "403": { "description": "Forbidden - the caller is not a member of the organization named by organization_id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } }, "404": { "description": "Not found - no releases found for the Spotify artist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } }, "504": { "description": "The measurement did not complete within the wait window - retry shortly", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } } } } } }, "components": { "schemas": { "RunValuationRequest": { "type": "object", "required": [ "spotify_artist_id" ], "properties": { "spotify_artist_id": { "type": "string", "description": "The Spotify artist id to value (e.g. from a Spotify artist search). The endpoint resolves this artist's releases, measures them, and builds the catalog." }, "organization_id": { "type": "string", "format": "uuid", "description": "Optional. ID of an organization to own the resulting catalog instead of the calling account. Every member of that organization then sees it in their own [Get Catalogs](/docs/api-reference/songs/catalogs) response. The caller must be a member of the organization; otherwise the request is rejected with 403. Credits are still spent by the calling account, and the searched artist is still linked to the caller's roster. Omit to own the catalog personally, which is the default." }, "account_id": { "type": "string", "format": "uuid", "description": "Optional. Act on behalf of this account (UUID). Only applicable when the authenticated account has access to the target account, for example an organization key acting for a member account. Every side effect of the run - the playcount snapshot, the catalog claim, the roster attach, and the report email - is scoped to this account instead of the caller. Omit to run for the caller's own account, which is the default." } } }, "RunValuationResponse": { "type": "object", "description": "Result of a valuation run: the materialized catalog and its estimated value band.", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "catalog": { "$ref": "#/components/schemas/Catalog" }, "band": { "$ref": "#/components/schemas/ValuationBand" }, "songs_measured": { "type": "integer", "description": "Number of tracks with a captured play count that were materialized into the catalog." }, "error": { "type": "string", "description": "Error message (only present if status is 'error')" } } }, "CatalogSongsErrorResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "Catalog": { "type": "object", "description": "A catalog with its metadata", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the catalog" }, "name": { "type": "string", "description": "Name of the catalog" }, "created_at": { "type": "string", "format": "date-time", "description": "ISO timestamp of when the catalog was created" }, "updated_at": { "type": "string", "format": "date-time", "description": "ISO timestamp of when the catalog was last updated" } } }, "ValuationBand": { "type": "object", "description": "Estimated catalog value in USD, derived at read time from the latest measurements - the same model as the recoupable.dev valuation card. Annual run-rate = lifetime streams / catalog age (lifetime-average proxy), converted to net label share (all-DSP gross-up 1.25/1.4/1.6, 15% distribution fee, 25% royalty share, $0.0035 per Spotify stream) and multiplied by a 10x/13x/16x master-catalog market multiple.", "properties": { "low": { "type": "number", "description": "Low estimate in USD (1.25x gross-up, 10x multiple)" }, "mid": { "type": "number", "description": "Central estimate in USD (1.4x gross-up, 13x multiple)" }, "high": { "type": "number", "description": "High estimate in USD (1.6x gross-up, 16x multiple)" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Runs Source: https://recoupable.dev/docs/api-reference/songs/runs Get runs ## GET /api/runs Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/runs": { "get": { "summary": "Get runs", "description": "Get background runs, newest first. A run is the generic status resource for long-running work. `kind` selects the run type (`valuation` and `music` today); future kinds are new enum values here, never new endpoints.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "kind", "in": "query", "required": true, "schema": { "type": "string", "enum": [ "valuation", "music" ] }, "description": "Required. The run type to list. Unknown kinds are rejected with 400." }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 20, "default": 1 }, "description": "Optional. Maximum runs to return, newest first. Defaults to 1 (the latest run)." } ], "responses": { "200": { "description": "The calling account's runs of the requested kind, newest first. Empty when the account has never run one.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GetRunsResponse" } } } }, "400": { "description": "Missing or unknown kind, or invalid limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } }, "401": { "description": "Missing or invalid credentials.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } } } } } }, "components": { "schemas": { "GetRunsResponse": { "type": "object", "description": "The calling account's runs of the requested kind, newest first.", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "runs": { "type": "array", "description": "Runs, newest first. Empty when the account has never run one of this kind. The item shape follows the requested `kind`.", "items": { "oneOf": [ { "title": "Valuation run", "$ref": "#/components/schemas/ValuationRun" }, { "title": "Music run", "$ref": "#/components/schemas/MusicRun" } ] } }, "error": { "type": "string", "description": "Error message (only present if status is 'error')" } } }, "CatalogSongsErrorResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "ValuationRun": { "type": "object", "description": "One background run. `id` is opaque; `state` is a domain phase, not a storage value: `queued` (capture accepted, not yet scraping), `measuring` (capture in flight, or finished moments ago and being claimed), `claimed` (catalog materialized - `result.catalog_id` is set), `failed` (the capture finished but no catalog was claimed, or the capture itself failed).", "properties": { "id": { "type": "string", "format": "uuid", "description": "Opaque run id. Do not infer anything from its format; it is stable for polling a single run across requests." }, "kind": { "type": "string", "enum": [ "valuation" ], "description": "The run type." }, "state": { "type": "string", "enum": [ "queued", "measuring", "claimed", "failed" ], "description": "Domain phase of the run." }, "album_count": { "type": "integer", "description": "Number of releases in the run's capture scope." }, "created_at": { "type": "string", "format": "date-time", "description": "When the run was created." }, "result": { "type": "object", "nullable": true, "description": "Set once the run is claimed; null before that.", "properties": { "catalog_id": { "type": "string", "format": "uuid", "description": "The materialized catalog. Read its value band via [Get Catalog Measurements](/docs/api-reference/songs/catalog-measurements)." } } } } }, "MusicRun": { "type": "object", "description": "One music generation, surfaced as a run. `id` is the generation's id: read the full record, including its logs, with [Get Music Generation](/docs/api-reference/music/get).", "properties": { "id": { "type": "string", "format": "uuid", "description": "The generation's id." }, "kind": { "type": "string", "enum": [ "music" ], "description": "The run type." }, "state": { "type": "string", "enum": [ "queued", "generating", "complete", "failed" ], "description": "Domain phase of the run: `queued` before the workflow picks it up, `generating` while the model renders, then `complete` or `failed`." }, "created_at": { "type": "string", "format": "date-time", "description": "When the generation was created." }, "result": { "type": "object", "nullable": true, "description": "Set once the generation completes; null before that.", "properties": { "generation_id": { "type": "string", "format": "uuid", "description": "The completed generation." }, "audio_url": { "type": "string", "description": "Playable audio URL." } } } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Catalog Measurements Source: https://recoupable.dev/docs/api-reference/songs/catalog-measurements Get the latest play counts and a derived valuation band for a catalog. Measurements are captured by [Create measurement job](/docs/api-reference/research/measurement-jobs) runs; the band is computed at read time from the latest capture per song. ## GET /api/catalogs/{catalogId}/measurements Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/catalogs/{catalogId}/measurements": { "get": { "description": "Get the latest play counts and a derived valuation band for a catalog. Measurements are captured by [Create measurement job](/docs/api-reference/research/measurement-jobs) runs; the band is computed at read time from the latest capture per song.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "catalogId", "in": "path", "description": "The unique identifier of the catalog to read measurements for. The catalog must belong to the authenticated account. Malformed (non-uuid) values are rejected with 400.", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "account_id", "in": "query", "description": "Act on behalf of this account (UUID). Only applicable when the authenticated account has access to the target account, for example an organization key acting for a member account. The catalog ownership check then runs against this account instead of the caller. Omitted: the caller's own account.", "required": false, "schema": { "type": "string", "format": "uuid" } }, { "name": "artist_account_id", "in": "query", "description": "Optional artist account to scope the read to. When present, measurements and the valuation band cover only the catalog's songs linked to this artist account (via song_artists); when absent, the whole catalog. A song linked to multiple artists counts for each of them. An artist with no linked measured songs in the catalog yields measured_song_count 0 and an empty measurements array. Malformed (non-uuid) values are rejected with 400.", "required": false, "schema": { "type": "string", "format": "uuid" } }, { "name": "page", "in": "query", "description": "Page number of the measurements array (default 1). Pagination only windows the measurements rows - the aggregates always cover the entire scope. Invalid values are rejected with 400.", "required": false, "schema": { "type": "integer", "minimum": 1, "default": 1 } }, { "name": "limit", "in": "query", "description": "Number of measurement rows per page (default 50, max 100). Invalid values are rejected with 400.", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } } ], "responses": { "200": { "description": "Latest per-song measurements and the derived valuation band", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogMeasurementsResponse" } } } }, "400": { "description": "Bad request - malformed catalogId or artist_account_id, or invalid page/limit", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } }, "404": { "description": "Not found - the catalog does not exist or belongs to a different account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } } } } } }, "components": { "schemas": { "CatalogMeasurementsResponse": { "type": "object", "description": "Latest per-song play counts for a catalog plus the valuation band derived from them", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "measurements": { "type": "array", "items": { "$ref": "#/components/schemas/CatalogTrackMeasurement" }, "description": "One page of latest-per-ISRC measurements (the requested page/limit window), sorted by play count descending. One entry per measured song: the newest capture per ISRC; songs never measured are omitted. Use measured_song_count / pagination for the size of the full set - the aggregates are NOT limited to this page." }, "pagination": { "$ref": "#/components/schemas/CatalogMeasurementsPagination" }, "measured_song_count": { "type": "integer", "description": "Total number of measured songs in scope (the whole catalog, or the artist subset when filtered), computed over the entire scope regardless of pagination - no row cap" }, "valuation": { "$ref": "#/components/schemas/ValuationBand" }, "total_streams": { "type": "integer", "description": "Sum of the latest play counts across ALL measured songs in scope, computed in a single SQL aggregate regardless of pagination - no row cap" }, "artist_account_id": { "type": "string", "format": "uuid", "nullable": true, "description": "Echoes the applied artist_account_id filter: the uuid when the response was scoped to that artist, null when the response covers the whole catalog. Clients that request an artist scope should verify this echo before treating the numbers as artist-scoped." }, "catalog_age_years": { "type": "integer", "description": "Catalog age in years used for the annual run-rate: from the earliest Spotify release date of the source run's albums, minimum 1; defaults to 5 when no release date is resolvable." }, "error": { "type": "string", "description": "Error message (only present if status is 'error')" } } }, "CatalogSongsErrorResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "CatalogTrackMeasurement": { "type": "object", "description": "The most recent Spotify play-count measurement for one song (ISRC) in the catalog", "properties": { "isrc": { "type": "string", "description": "ISRC of the song" }, "title": { "type": "string", "nullable": true, "description": "Song title, when known" }, "playcount": { "type": "integer", "description": "Latest Spotify platform-displayed play count" }, "measured_at": { "type": "string", "format": "date-time", "description": "ISO timestamp of when the play count was captured" } } }, "CatalogMeasurementsPagination": { "type": "object", "description": "Pagination metadata for the measurements array - the same envelope as catalog songs", "properties": { "total_count": { "type": "integer", "description": "Total number of measured songs in scope (equals measured_song_count)" }, "page": { "type": "integer", "description": "Current page number" }, "limit": { "type": "integer", "description": "Number of measurement rows per page" }, "total_pages": { "type": "integer", "description": "Total number of pages available" } } }, "ValuationBand": { "type": "object", "description": "Estimated catalog value in USD, derived at read time from the latest measurements - the same model as the recoupable.dev valuation card. Annual run-rate = lifetime streams / catalog age (lifetime-average proxy), converted to net label share (all-DSP gross-up 1.25/1.4/1.6, 15% distribution fee, 25% royalty share, $0.0035 per Spotify stream) and multiplied by a 10x/13x/16x master-catalog market multiple.", "properties": { "low": { "type": "number", "description": "Low estimate in USD (1.25x gross-up, 10x multiple)" }, "mid": { "type": "number", "description": "Central estimate in USD (1.4x gross-up, 13x multiple)" }, "high": { "type": "number", "description": "High estimate in USD (1.6x gross-up, 16x multiple)" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Catalog Playcount History Source: https://recoupable.dev/docs/api-reference/songs/catalog-playcount-history Read catalog playcount history and period comparisons ## GET /api/catalogs/{catalogId}/playcount-history Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/catalogs/{catalogId}/playcount-history": { "get": { "summary": "Read catalog playcount history and period comparisons", "description": "Read-only, store-backed public Spotify observations for current catalog membership, including unmeasured recordings. No collection, provider calls, charges or cron activation. API keys and Privy bearer tokens supported; identity overrides, unknown and duplicate query parameters rejected. Compare two adjacent equal observation periods with complete daily coverage and at most one hour capture drift. Missing days, invalid counts, negative corrections and excessive drift suppress growth. Real zero values remain zero; zero prior movement produces null percentage growth. Summaries apply only to this page; increment page while pagination.has_more. Catalog edits can change paging. Legacy rows lack historical provider counter identity and upstream update time: comparisons are provisional, not verified same-counter history, exact daily streams, royalties or causal marketing uplift. This endpoint does not import private artist analytics. Standard MCP tool: get_catalog_playcount_history; unavailable for delegated OAuth pending organization-grant audit. Proposed endpoint requires the linked API feature release; documentation is not proof of production availability.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "catalogId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "Catalog owned by the authenticated account or an organization it currently belongs to." }, { "name": "since", "in": "query", "required": true, "schema": { "type": "string", "format": "date" }, "description": "Current UTC observation-period start. Previous period immediately precedes it. Boundaries use the latest observation on that date, not midnight stream totals." }, { "name": "days", "in": "query", "required": true, "schema": { "type": "integer", "minimum": 1, "maximum": 31 }, "description": "Equal length of both periods. Every UTC observation day through the final boundary must be finished and captured." }, { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 1000000, "default": 1 } }, { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 25, "default": 25 } } ], "responses": { "200": { "description": "Saved observations and page-scoped comparison coverage", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "success" ] }, "catalog_id": { "type": "string", "format": "uuid" }, "platform": { "type": "string", "enum": [ "spotify" ] }, "metric": { "type": "string", "enum": [ "platform_displayed_play_count" ] }, "data_source": { "type": "string", "enum": [ "apify_spotify_playcount" ] }, "semantics": { "type": "string" }, "source_timestamp_available": { "type": "boolean", "enum": [ false ] }, "provider_identity_available": { "type": "boolean", "enum": [ false ] }, "comparison_quality": { "type": "string", "enum": [ "legacy_recording_series_identity_unverified" ] }, "missing_observation_reason_available": { "type": "boolean", "enum": [ false ] }, "collection_enabled": { "type": "boolean", "enum": [ false ] }, "periods": { "type": "object", "properties": { "previous": { "type": "object", "properties": { "start": { "type": "string", "format": "date" }, "end": { "type": "string", "format": "date" } } }, "current": { "type": "object", "properties": { "start": { "type": "string", "format": "date" }, "end": { "type": "string", "format": "date" } } }, "days": { "type": "integer" }, "timezone": { "type": "string", "enum": [ "UTC" ] }, "boundary_semantics": { "type": "string" }, "alignment_tolerance_seconds": { "type": "integer", "enum": [ 3600 ] } } }, "pagination": { "type": "object", "properties": { "page": { "type": "integer" }, "limit": { "type": "integer" }, "total_count": { "type": "integer" }, "total_pages": { "type": "integer" }, "has_more": { "type": "boolean" } } }, "summary_scope": { "type": "string", "enum": [ "page" ] }, "comparable_recordings": { "type": "integer" }, "recordings": { "type": "array", "items": { "type": "object", "properties": { "isrc": { "type": "string" }, "name": { "type": [ "string", "null" ] }, "state": { "type": "string", "enum": [ "comparable", "incomplete", "counter_correction", "invalid_observation", "unaligned_observations" ] }, "observations": { "type": "array", "items": { "type": "object", "properties": { "date": { "type": "string", "format": "date" }, "captured_at": { "type": "string", "format": "date-time" }, "value": { "type": "number" } } } }, "missing_days": { "type": "array", "items": { "type": "string", "format": "date" } }, "invalid_days": { "type": "array", "items": { "type": "string", "format": "date" } }, "correction_days": { "type": "array", "items": { "type": "string", "format": "date" } }, "previous_change": { "type": [ "number", "null" ] }, "current_change": { "type": [ "number", "null" ] }, "absolute_growth": { "type": [ "number", "null" ] }, "percentage_growth": { "type": [ "number", "null" ], "description": "Percentage change in period counter movement; null for a zero baseline or non-comparable series." }, "zero_baseline": { "type": "boolean" } } } } } } } } }, "400": { "description": "Invalid or unfinished observation-period query" }, "401": { "description": "Missing or invalid credentials" }, "404": { "description": "Catalog missing or inaccessible" }, "503": { "description": "Store unavailable or history exceeds bounded read; use a shorter period" } } } } }, "components": { "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Catalog Valuations Source: https://recoupable.dev/docs/api-reference/songs/catalog-valuations **Availability: pending reconciliation.** Valuation-history readback has no matching route in the inspected API source. Valuation calculation and history persistence/readback are separate outcomes; calculation alone does not prove a saved, account-scoped history. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation. ## GET /api/catalogs/{catalogId}/valuations Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/catalogs/{catalogId}/valuations": { "get": { "description": "**Availability: pending reconciliation.** Valuation-history readback has no matching route in the inspected API source. Valuation calculation and history persistence/readback are separate outcomes; calculation alone does not prove a saved, account-scoped history. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation.\n\nGet the persisted valuation history for a catalog, latest first. A row is written each time a valuation band is computed for the whole catalog (valuation runs and measurement reads persist at most one row per catalog per day). Use limit=1 for the current value. History is what makes week-over-week deltas possible.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "catalogId", "in": "path", "description": "The unique identifier of the catalog. The catalog must belong to the authenticated account. Malformed (non-uuid) values are rejected with 400.", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "limit", "in": "query", "description": "Maximum number of valuation rows to return, latest first (default 30, max 100). limit=1 returns the current value. Invalid values are rejected with 400.", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 30 } } ], "responses": { "200": { "description": "The catalog's persisted valuation rows, latest first. Empty when no valuation has been persisted yet.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogValuationsResponse" } } } }, "400": { "description": "Malformed catalogId or limit", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Catalog not found or not owned by the authenticated account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "x-recoup-availability": "pending" } } }, "components": { "schemas": { "CatalogValuationsResponse": { "type": "object", "required": [ "status", "valuations" ], "properties": { "status": { "type": "string", "example": "success" }, "valuations": { "type": "array", "description": "Persisted valuation rows, latest first.", "items": { "type": "object", "required": [ "low", "mid", "high", "measured_song_count", "total_streams", "measured_at" ], "properties": { "low": { "type": "number", "description": "Low end of the estimated catalog value band, USD." }, "mid": { "type": "number", "description": "Midpoint of the estimated catalog value band, USD." }, "high": { "type": "number", "description": "High end of the estimated catalog value band, USD." }, "measured_song_count": { "type": "integer", "description": "Songs measured in the capture this valuation was computed from." }, "total_streams": { "type": "integer", "description": "Whole-catalog lifetime stream total at measurement time." }, "measured_at": { "type": "string", "format": "date-time", "description": "When the underlying measurement was taken." } } } } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Catalog Songs Source: https://recoupable.dev/docs/api-reference/songs/catalog-songs Retrieve songs within a specific catalog with pagination support. This endpoint joins catalog_songs with songs, song_artists, and accounts to provide comprehensive song information for a given catalog. > **Note** > This endpoint supports pagination. The Catalog Songs API also supports POST for adding songs and DELETE for removing songs. See the [Add Catalog Songs](https://recoupable.dev/docs/api-reference/songs/catalog-songs-add) and [Remove Catalog Songs](https://recoupable.dev/docs/api-reference/songs/catalog-songs-delete) endpoints. ## GET /api/catalogs/songs Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/catalogs/songs": { "get": { "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "description": "Retrieve songs within a specific catalog with pagination support. This endpoint joins catalog_songs with songs, song_artists, and accounts to provide comprehensive song information for a given catalog.", "parameters": [ { "name": "catalog_id", "in": "query", "description": "The unique identifier of the catalog to query songs for", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "artistName", "in": "query", "description": "Optional. Filters songs to only include those with matching artist name", "required": false, "schema": { "type": "string" } }, { "name": "page", "in": "query", "description": "Page number for pagination (default: 1)", "required": false, "schema": { "type": "integer", "default": 1 } }, { "name": "limit", "in": "query", "description": "Number of songs per page (default: 20, max: 100)", "required": false, "schema": { "type": "integer", "default": 20, "maximum": 100 } } ], "responses": { "200": { "description": "Catalog songs retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsResponse" } } } }, "400": { "description": "Bad request - missing or invalid catalog_id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } }, "403": { "description": "Forbidden - the catalog does not belong to the authenticated account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } }, "404": { "description": "Catalog not found or no songs in catalog", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } } } } } }, "components": { "schemas": { "CatalogSongsResponse": { "type": "object", "description": "Response containing catalog songs data with pagination", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "songs": { "type": "array", "items": { "$ref": "#/components/schemas/CatalogSong" }, "description": "Array of song objects with artist information" }, "pagination": { "$ref": "#/components/schemas/CatalogSongsPagination", "description": "Pagination metadata for the response" }, "error": { "type": "string", "description": "Error message (only present if status is 'error')" } } }, "CatalogSongsErrorResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "CatalogSong": { "type": "object", "description": "A song within a catalog with its metadata and associated artists", "properties": { "catalog_id": { "type": "string", "format": "uuid", "description": "Catalog ID this song entry is associated with" }, "isrc": { "type": "string", "description": "International Standard Recording Code (primary key)" }, "name": { "type": "string", "description": "Name of the song" }, "album": { "type": "string", "description": "Name of the album the song belongs to" }, "lyrics": { "type": "string", "description": "Full lyrics of the song" }, "updated_at": { "type": "string", "format": "date-time", "description": "ISO timestamp of when the song data was last updated" }, "artists": { "type": "array", "items": { "$ref": "#/components/schemas/SongArtist" }, "description": "Array of artist objects associated with this song" } } }, "CatalogSongsPagination": { "type": "object", "description": "Pagination metadata for catalog songs response", "properties": { "total_count": { "type": "integer", "description": "Total number of songs in the catalog" }, "page": { "type": "integer", "description": "Current page number" }, "limit": { "type": "integer", "description": "Number of songs per page" }, "total_pages": { "type": "integer", "description": "Total number of pages available" } } }, "SongArtist": { "type": "object", "description": "Artist associated with a song", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the artist account" }, "name": { "type": "string", "nullable": true, "description": "Name of the artist (can be null)" }, "timestamp": { "type": "integer", "nullable": true, "description": "Timestamp associated with the artist account (can be null)" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Add Catalog Songs Source: https://recoupable.dev/docs/api-reference/songs/catalog-songs-add Batch add songs to a catalog by ISRC. For each song, the API attempts to look up metadata via internal search. If no data is found, optional fallback fields (name, album, notes, artists) are used. > **Note** > For each song, the API attempts to look up metadata via internal search. If no data is found, optional fallback fields (name, album, notes, artists) are used. ## POST /api/catalogs/songs Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/catalogs/songs": { "post": { "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "description": "Batch add songs to a catalog by ISRC. For each song, the API attempts to look up metadata via internal search. If no data is found, optional fallback fields (name, album, notes, artists) are used.", "requestBody": { "description": "Array of songs to add to catalog", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AddCatalogSongsRequest" } } } }, "responses": { "200": { "description": "Songs added to catalog successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsResponse" } } } }, "400": { "description": "Bad request - missing required fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } }, "403": { "description": "Forbidden - the catalog does not belong to the authenticated account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } } } } } }, "components": { "schemas": { "AddCatalogSongsRequest": { "type": "object", "required": [ "songs" ], "properties": { "songs": { "type": "array", "items": { "$ref": "#/components/schemas/AddCatalogSongInput" }, "description": "Array of songs for batch updates" } } }, "CatalogSongsResponse": { "type": "object", "description": "Response containing catalog songs data with pagination", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "songs": { "type": "array", "items": { "$ref": "#/components/schemas/CatalogSong" }, "description": "Array of song objects with artist information" }, "pagination": { "$ref": "#/components/schemas/CatalogSongsPagination", "description": "Pagination metadata for the response" }, "error": { "type": "string", "description": "Error message (only present if status is 'error')" } } }, "CatalogSongsErrorResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "AddCatalogSongInput": { "type": "object", "required": [ "catalog_id", "isrc" ], "properties": { "catalog_id": { "type": "string", "format": "uuid", "description": "Catalog ID to which the song will be added" }, "isrc": { "type": "string", "description": "Song ISRC to associate to the catalog" }, "name": { "type": "string", "description": "Optional. Applied only if internal search cannot find valid info for ISRC" }, "album": { "type": "string", "description": "Optional. Applied only if internal search cannot find valid info for ISRC" }, "notes": { "type": "string", "description": "Optional. Applied only if internal search cannot find valid info for ISRC" }, "artists": { "type": "array", "items": { "type": "string" }, "description": "Optional array of artist names. Applied only if internal search lacks info" } } }, "CatalogSong": { "type": "object", "description": "A song within a catalog with its metadata and associated artists", "properties": { "catalog_id": { "type": "string", "format": "uuid", "description": "Catalog ID this song entry is associated with" }, "isrc": { "type": "string", "description": "International Standard Recording Code (primary key)" }, "name": { "type": "string", "description": "Name of the song" }, "album": { "type": "string", "description": "Name of the album the song belongs to" }, "lyrics": { "type": "string", "description": "Full lyrics of the song" }, "updated_at": { "type": "string", "format": "date-time", "description": "ISO timestamp of when the song data was last updated" }, "artists": { "type": "array", "items": { "$ref": "#/components/schemas/SongArtist" }, "description": "Array of artist objects associated with this song" } } }, "CatalogSongsPagination": { "type": "object", "description": "Pagination metadata for catalog songs response", "properties": { "total_count": { "type": "integer", "description": "Total number of songs in the catalog" }, "page": { "type": "integer", "description": "Current page number" }, "limit": { "type": "integer", "description": "Number of songs per page" }, "total_pages": { "type": "integer", "description": "Total number of pages available" } } }, "SongArtist": { "type": "object", "description": "Artist associated with a song", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the artist account" }, "name": { "type": "string", "nullable": true, "description": "Name of the artist (can be null)" }, "timestamp": { "type": "integer", "nullable": true, "description": "Timestamp associated with the artist account (can be null)" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Remove Catalog Songs Source: https://recoupable.dev/docs/api-reference/songs/catalog-songs-delete Batch remove songs from a catalog by ISRC. Deletes the relationship in catalog_songs for each catalog_id and ISRC pair. > **Note** > This endpoint removes the relationship in `catalog_songs` for each `catalog_id` and ISRC pair. The song data itself is not deleted. ## DELETE /api/catalogs/songs Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/catalogs/songs": { "delete": { "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "description": "Batch remove songs from a catalog by ISRC. Deletes the relationship in catalog_songs for each catalog_id and ISRC pair.", "requestBody": { "description": "Array of songs to remove from catalog", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteCatalogSongsRequest" } } } }, "responses": { "200": { "description": "Songs removed from catalog successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsResponse" } } } }, "400": { "description": "Bad request - missing catalog_id or isrc", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } }, "403": { "description": "Forbidden - the catalog does not belong to the authenticated account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogSongsErrorResponse" } } } } } } } }, "components": { "schemas": { "DeleteCatalogSongsRequest": { "type": "object", "required": [ "songs" ], "properties": { "songs": { "type": "array", "items": { "$ref": "#/components/schemas/DeleteCatalogSongInput" }, "description": "Array of songs for batch deletes" } } }, "CatalogSongsResponse": { "type": "object", "description": "Response containing catalog songs data with pagination", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "songs": { "type": "array", "items": { "$ref": "#/components/schemas/CatalogSong" }, "description": "Array of song objects with artist information" }, "pagination": { "$ref": "#/components/schemas/CatalogSongsPagination", "description": "Pagination metadata for the response" }, "error": { "type": "string", "description": "Error message (only present if status is 'error')" } } }, "CatalogSongsErrorResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "DeleteCatalogSongInput": { "type": "object", "required": [ "catalog_id", "isrc" ], "properties": { "catalog_id": { "type": "string", "format": "uuid", "description": "Catalog ID from which the song will be removed" }, "isrc": { "type": "string", "description": "Song ISRC to remove from the catalog" } } }, "CatalogSong": { "type": "object", "description": "A song within a catalog with its metadata and associated artists", "properties": { "catalog_id": { "type": "string", "format": "uuid", "description": "Catalog ID this song entry is associated with" }, "isrc": { "type": "string", "description": "International Standard Recording Code (primary key)" }, "name": { "type": "string", "description": "Name of the song" }, "album": { "type": "string", "description": "Name of the album the song belongs to" }, "lyrics": { "type": "string", "description": "Full lyrics of the song" }, "updated_at": { "type": "string", "format": "date-time", "description": "ISO timestamp of when the song data was last updated" }, "artists": { "type": "array", "items": { "$ref": "#/components/schemas/SongArtist" }, "description": "Array of artist objects associated with this song" } } }, "CatalogSongsPagination": { "type": "object", "description": "Pagination metadata for catalog songs response", "properties": { "total_count": { "type": "integer", "description": "Total number of songs in the catalog" }, "page": { "type": "integer", "description": "Current page number" }, "limit": { "type": "integer", "description": "Number of songs per page" }, "total_pages": { "type": "integer", "description": "Total number of pages available" } } }, "SongArtist": { "type": "object", "description": "Artist associated with a song", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the artist account" }, "name": { "type": "string", "nullable": true, "description": "Name of the artist (can be null)" }, "timestamp": { "type": "integer", "nullable": true, "description": "Timestamp associated with the artist account (can be null)" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Daily Catalog Streams Source: https://recoupable.dev/docs/api-reference/songs/catalog-streams Read daily catalog streams ## GET /api/catalogs/{catalogId}/streams Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/catalogs/{catalogId}/streams": { "get": { "summary": "Read daily catalog streams", "description": "Saved Luminate worldwide aggregate calendar-day streams across reporting DSPs, not Spotify-only or royalty statements. Includes current catalog recordings even when unmeasured, latest collection status, per-recording daily rows/retrieval time, missing dates, equal-period totals and growth. Newer source corrections win; historical versions remain stored. Missing days and changed provider recording identity suppress comparisons. Zero baseline yields null percentage. Page through pagination.has_more; summary_scope is page. Reads make no provider requests. Standard MCP: get_catalog_streams. Requires API and database feature release; unavailable to delegated OAuth. Unchanged daily values reuse their saved versions; new dates and corrections are appended, with fresh coverage/provenance receipts for every run.\n\nUse paging for large catalogs. Accepted date ranges do not establish measurement completeness; inspect per-record coverage and missing data.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "catalogId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "Catalog owned by the authenticated account or an organization it currently belongs to." }, { "name": "since", "in": "query", "required": true, "schema": { "type": "string", "format": "date" }, "description": "Current period start, inclusive. Previous period has the same length and immediately precedes it." }, { "name": "days", "in": "query", "required": true, "schema": { "type": "integer", "minimum": 1, "maximum": 366 }, "description": "Comparison period length, 1–366 days; end dates are exclusive. Missing measurements suppress growth. This range does not imply complete backfill. The separate catalog playcount endpoint retains its 31-day limit.", "example": 90 }, { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 1000000, "default": 1 } }, { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 25, "default": 25 } } ], "responses": { "200": { "description": "Saved history and comparisons", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string" }, "catalog_id": { "type": "string", "format": "uuid" }, "provider": { "type": "string", "enum": [ "luminate" ] }, "platform": { "type": "string", "enum": [ "all_dsps" ] }, "metric": { "type": "string", "enum": [ "daily_streams" ] }, "latest_run": { "$ref": "#/components/schemas/CatalogStreamRun", "description": "Run status, per-ISRC coverage, errors, window and completion timestamp." }, "territory": { "type": "string", "enum": [ "worldwide" ] }, "semantics": { "type": "string" }, "source_updated_at": { "type": "null" }, "periods": { "type": "object", "properties": { "previous": { "type": "object", "properties": { "start": { "type": "string", "format": "date" }, "end_exclusive": { "type": "string", "format": "date" } } }, "current": { "type": "object", "properties": { "start": { "type": "string", "format": "date" }, "end_exclusive": { "type": "string", "format": "date" } } }, "days": { "type": "integer" }, "timezone": { "type": "string", "enum": [ "UTC" ] } } }, "pagination": { "type": "object", "properties": { "page": { "type": "integer" }, "limit": { "type": "integer" }, "total_count": { "type": "integer" }, "has_more": { "type": "boolean" } } }, "summary_scope": { "type": "string", "enum": [ "page" ] }, "recordings": { "type": "array", "items": { "type": "object", "properties": { "isrc": { "type": "string" }, "name": { "type": [ "string", "null" ] }, "provider_recording_id": { "type": [ "string", "null" ] }, "retrieved_at": { "type": [ "string", "null" ], "format": "date-time" }, "days": { "type": "array", "items": { "type": "object", "properties": { "date": { "type": "string", "format": "date" }, "streams": { "type": [ "integer", "null" ] } }, "required": [ "date", "streams" ] } }, "state": { "type": "string", "enum": [ "comparable", "incomplete", "unfinished_period", "invalid_observation" ] }, "missing_days": { "type": "array", "items": { "type": "string", "format": "date" } }, "previous_streams": { "type": [ "integer", "null" ] }, "current_streams": { "type": [ "integer", "null" ] }, "absolute_growth": { "type": [ "integer", "null" ] }, "percentage_growth": { "type": [ "number", "null" ] }, "zero_baseline": { "type": "boolean" }, "latest_attempt": { "$ref": "#/components/schemas/CatalogStreamReceipt", "description": "Fresh recording-level receipt for the latest run, including state, observed_days, retrieved_at, source_hash and provider_recording_id. Observation retrieved_at is when its retained version was introduced." } } } } } } } } }, "400": { "description": "Invalid input or incomplete calendar period" }, "401": { "description": "Authentication required" }, "404": { "description": "Catalog not accessible" }, "503": { "description": "Storage, configuration or dispatch unavailable" } } } } }, "components": { "schemas": { "CatalogStreamRun": { "type": [ "object", "null" ], "properties": { "id": { "type": "string", "format": "uuid" }, "catalog_id": { "type": "string", "format": "uuid" }, "revision": { "type": "string", "format": "uuid" }, "scheduled_day": { "type": "string", "format": "date" }, "since": { "type": "string", "format": "date" }, "until": { "type": "string", "format": "date" }, "status": { "type": "string", "enum": [ "queued", "running", "complete", "partial", "failed", "cancelled" ] }, "coverage": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/CatalogStreamReceipt" } }, "error": { "type": [ "string", "null" ] }, "created_at": { "type": "string", "format": "date-time" }, "finished_at": { "type": [ "string", "null" ], "format": "date-time" } } }, "CatalogStreamReceipt": { "type": [ "object", "null" ], "properties": { "state": { "type": "string", "enum": [ "complete", "partial", "unavailable", "failed" ] }, "observed_days": { "type": "integer", "minimum": 0, "maximum": 62 }, "retrieved_at": { "type": [ "string", "null" ], "format": "date-time" }, "source_hash": { "type": [ "string", "null" ] }, "provider_recording_id": { "type": [ "string", "null" ] } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Catalog Stream Tracking Source: https://recoupable.dev/docs/api-reference/songs/catalog-stream-tracking Inspect daily catalog tracking ## GET /api/catalogs/{catalogId}/stream-tracking Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/catalogs/{catalogId}/stream-tracking": { "get": { "summary": "Inspect daily catalog tracking", "description": "Read enabled state and latest run coverage without starting provider work. Standard MCP manage_catalog_stream_tracking with action=status. Unchanged daily values reuse their saved versions; new dates and corrections are appended, with fresh coverage/provenance receipts for every run.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "catalogId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "Catalog owned by the authenticated account or an organization it currently belongs to." } ], "responses": { "200": { "description": "Collection status", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string" }, "catalog_id": { "type": "string", "format": "uuid" }, "provider": { "type": "string", "enum": [ "luminate" ] }, "platform": { "type": "string", "enum": [ "all_dsps" ] }, "metric": { "type": "string", "enum": [ "daily_streams" ] }, "latest_run": { "$ref": "#/components/schemas/CatalogStreamRun", "description": "Run status, per-ISRC coverage, errors, window and completion timestamp." }, "territory": { "type": "string", "enum": [ "worldwide" ] }, "tracking": { "type": [ "object", "null" ], "properties": { "catalog_id": { "type": "string", "format": "uuid" }, "owner_id": { "type": "string", "format": "uuid" }, "enabled": { "type": "boolean" }, "revision": { "type": "string", "format": "uuid" }, "updated_at": { "type": "string", "format": "date-time" } } }, "collection": { "type": [ "object", "null" ], "properties": { "state": { "type": "string", "enum": [ "started", "already_claimed_or_disabled" ] }, "run_id": { "type": [ "string", "null" ], "format": "uuid" }, "workflow_run_id": { "type": "string" } } } } } } } }, "400": { "description": "Invalid input" }, "401": { "description": "Authentication required" }, "404": { "description": "Catalog not accessible" }, "503": { "description": "Storage, configuration or dispatch unavailable" } } } } }, "components": { "schemas": { "CatalogStreamRun": { "type": [ "object", "null" ], "properties": { "id": { "type": "string", "format": "uuid" }, "catalog_id": { "type": "string", "format": "uuid" }, "revision": { "type": "string", "format": "uuid" }, "scheduled_day": { "type": "string", "format": "date" }, "since": { "type": "string", "format": "date" }, "until": { "type": "string", "format": "date" }, "status": { "type": "string", "enum": [ "queued", "running", "complete", "partial", "failed", "cancelled" ] }, "coverage": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/CatalogStreamReceipt" } }, "error": { "type": [ "string", "null" ] }, "created_at": { "type": "string", "format": "date-time" }, "finished_at": { "type": [ "string", "null" ], "format": "date-time" } } }, "CatalogStreamReceipt": { "type": [ "object", "null" ], "properties": { "state": { "type": "string", "enum": [ "complete", "partial", "unavailable", "failed" ] }, "observed_days": { "type": "integer", "minimum": 0, "maximum": 62 }, "retrieved_at": { "type": [ "string", "null" ], "format": "date-time" }, "source_hash": { "type": [ "string", "null" ] }, "provider_recording_id": { "type": [ "string", "null" ] } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Update Catalog Stream Tracking Source: https://recoupable.dev/docs/api-reference/songs/update-catalog-stream-tracking Control daily catalog tracking ## POST /api/catalogs/{catalogId}/stream-tracking Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/catalogs/{catalogId}/stream-tracking": { "post": { "summary": "Control daily catalog tracking", "description": "Enable starts initial 62-day backfill and daily refresh at 09:00 UTC. Source window ends two UTC dates before the run date to allow reporting lag; source freshness is not guaranteed. New recordings join the next run. Maximum 250 recordings per catalog; larger catalogs fail before provider calls. Disable fences future writes and retains saved history. Refresh claims at most one run per UTC day and subscription revision; repeated enable is idempotent. If today was already claimed, collection.state=already_claimed_or_disabled and no duplicate provider work starts; failed claims can run again the next day. Failures are isolated by recording and exposed in coverage. Standard MCP manage_catalog_stream_tracking. Enabled tracking makes asynchronous provider calls. API-key/Privy bearer auth supported; identity overrides rejected. Requires API and database feature release. Unchanged daily values reuse their saved versions; new dates and corrections are appended, with fresh coverage/provenance receipts for every run.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "catalogId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "Catalog owned by the authenticated account or an organization it currently belongs to." } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "action" ], "additionalProperties": false, "properties": { "action": { "type": "string", "enum": [ "enable", "disable", "refresh" ] } } } } } }, "responses": { "200": { "description": "Tracking state and asynchronous collection receipt", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string" }, "catalog_id": { "type": "string", "format": "uuid" }, "provider": { "type": "string", "enum": [ "luminate" ] }, "platform": { "type": "string", "enum": [ "all_dsps" ] }, "metric": { "type": "string", "enum": [ "daily_streams" ] }, "latest_run": { "$ref": "#/components/schemas/CatalogStreamRun", "description": "Run status, per-ISRC coverage, errors, window and completion timestamp." }, "territory": { "type": "string", "enum": [ "worldwide" ] }, "tracking": { "type": [ "object", "null" ], "properties": { "catalog_id": { "type": "string", "format": "uuid" }, "owner_id": { "type": "string", "format": "uuid" }, "enabled": { "type": "boolean" }, "revision": { "type": "string", "format": "uuid" }, "updated_at": { "type": "string", "format": "date-time" } } }, "collection": { "type": [ "object", "null" ], "properties": { "state": { "type": "string", "enum": [ "started", "already_claimed_or_disabled" ] }, "run_id": { "type": [ "string", "null" ], "format": "uuid" }, "workflow_run_id": { "type": "string" } } } } } } } }, "400": { "description": "Invalid input" }, "401": { "description": "Authentication required" }, "404": { "description": "Catalog not accessible" }, "409": { "description": "Tracking must be enabled before refresh" }, "429": { "description": "Too many tracking control requests; limited to 10 per account per minute across REST and MCP." }, "503": { "description": "Storage, configuration or dispatch unavailable" } } } } }, "components": { "schemas": { "CatalogStreamRun": { "type": [ "object", "null" ], "properties": { "id": { "type": "string", "format": "uuid" }, "catalog_id": { "type": "string", "format": "uuid" }, "revision": { "type": "string", "format": "uuid" }, "scheduled_day": { "type": "string", "format": "date" }, "since": { "type": "string", "format": "date" }, "until": { "type": "string", "format": "date" }, "status": { "type": "string", "enum": [ "queued", "running", "complete", "partial", "failed", "cancelled" ] }, "coverage": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/CatalogStreamReceipt" } }, "error": { "type": [ "string", "null" ] }, "created_at": { "type": "string", "format": "date-time" }, "finished_at": { "type": [ "string", "null" ], "format": "date-time" } } }, "CatalogStreamReceipt": { "type": [ "object", "null" ], "properties": { "state": { "type": "string", "enum": [ "complete", "partial", "unavailable", "failed" ] }, "observed_days": { "type": "integer", "minimum": 0, "maximum": 62 }, "retrieved_at": { "type": [ "string", "null" ], "format": "date-time" }, "source_hash": { "type": [ "string", "null" ] }, "provider_recording_id": { "type": [ "string", "null" ] } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Context Engine Source: https://recoupable.dev/docs/api-reference/context/operate Ingest, read, or compile context briefs ## POST /api/context Full OpenAPI specification: https://recoupable.dev/docs/spec/context.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup Context Engine", "version": "0.1.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/context": { "post": { "operationId": "contextOperation", "summary": "Ingest, read, or compile context briefs", "description": "Save and reuse evidence in an authorized Recoup workspace. **Preview: this reference is verified against API source, not a completed hosted acceptance test.** Actions require the corresponding database and workflow release; verification actions also require their feature to be enabled. Do not infer provider readiness, complete coverage, or a successful workflow from an accepted request schema.\n\n### Create context\n\n`ingest` creates or reuses a Spotify-track request and dispatches its durable workflow (202). The parser recognizes YouTube URLs, but ingestion still rejects them in this pilot. `ingest_release` stores a Spotify album locator; it does not automatically verify the release. `ingest_catalog` and `ingest_artist` refer to accessible Recoup records. `ingest_songwriter_name`, `ingest_company_name`, `ingest_campaign_brief`, and `ingest_supporting_text` store submitted context (200). Names and customer text are not verified identities or facts. Reuse an idempotency key only for the same intended input.\n\n### Read, plan and inspect\n\n`read` reads one request. `plan` plans modules from stored context; a plan is not authorization to collect data. `list_executions` and `read_execution` expose execution records. `list_catalog_members` pages saved members; `expand_catalog_members` materializes a bounded member page. These operations return 200. Request IDs, subject IDs and execution IDs refer to different records; use the IDs returned by the corresponding read rather than substituting an account ID. Catalog membership does not establish rights ownership. The implementation also accepts plan_catalog_members, which is intentionally omitted from this preview request union pending a provider-neutral public module contract. This is a remaining documentation and API design gap, not an access restriction.\n\n### Release verification and review\n\n`verify_release` and `verify_release_tracks` enqueue enabled verification workflows (202); disabled features or unmet readiness conditions return 409. `list_release_tracks`, `read_release_track_observations`, and `review_release_track_identities` inspect saved slots and identity evidence (200). Inspect the returned readiness, coverage and unresolved identity state before using a recording match. `list_release_cases`, `read_release_case`, `read_release_case_review`, and `review_release_case` read or record release-case review. A review requires the current fingerprint, a decision, and an idempotency key; it does not silently rewrite source evidence.\n\n### Compile and save briefs\n\n`brief` compiles evidence from up to ten completed or partial requests in one workspace without provider or model calls. Purposes are `creative_direction`, `playlist_pitch`, and `company_onboarding`. Creative direction prioritizes lyrics and artwork; playlist pitching prioritizes catalog/audio metadata. Inspect readiness, request_coverage, selected input versions and gaps. Briefs are evidence packets, not finished creative concepts or outreach. `save_brief` freezes the server-compiled output, and `read_brief` retrieves that snapshot without recompilation. Newer evidence can supersede a snapshot without rewriting it; withdrawn evidence or removed scope can withhold its payload. All three actions return 200.\n\n### Scope and failures\n\nAuthentication determines the account. Optional organization_id selects an authorized organization workspace; request and subject ownership are checked again by the operation. Invalid request syntax returns 400, missing authentication 401, rejected organization access 403, and operation conflicts, unavailable evidence or disabled verification 409. No action accepts a caller-supplied account identity. Local schema tests do not establish hosted availability.\n\n### MCP mapping\n\nStandard MCP uses the `context` tool with the same action and fields as this REST request. Delegated OAuth exposes the named tools below and supplies the action server-side; omit action from the named tool input. These registrations require the full `mcp:tools` grant, current ownership and fresh credential verification. Read-only is a tool annotation, not a guarantee of hosted readiness. Discover the actual tools in the connected client before use; this is a static source mapping, not a verified client catalog. The omitted provider-bound planning action is still a remaining contract gap.\n\n| REST action | Delegated OAuth tool | Classification |\n| --- | --- | --- |\n| `list_release_cases` | `list_music_release_cases` | Read-only hint |\n| `read_release_case` | `read_music_release_case` | Read-only hint |\n| `read_release_case_review` | `read_music_release_case_review` | Read-only hint |\n| `review_release_case` | `record_music_release_case_review` | Writes or dispatches |\n| `save_brief` | `save_music_context_brief` | Writes or dispatches |\n| `read_brief` | `read_music_context_brief` | Read-only hint |\n| `plan` | `plan_music_context_research` | Read-only hint |\n| `list_executions` | `list_music_context_executions` | Read-only hint |\n| `list_catalog_members` | `list_music_context_catalog_members` | Read-only hint |\n| `list_release_tracks` | `list_music_context_release_tracks` | Read-only hint |\n| `read_release_track_observations` | `read_music_release_track_observations` | Read-only hint |\n| `review_release_track_identities` | `review_music_release_track_identities` | Read-only hint |\n| `verify_release_tracks` | `verify_music_release_tracks` | Writes or dispatches |\n| `expand_catalog_members` | `expand_music_context_catalog` | Writes or dispatches |\n| `read_execution` | `read_music_context_execution` | Read-only hint |\n| `ingest_catalog` | `import_catalog_context` | Writes or dispatches |\n| `ingest_artist` | `import_artist_context` | Writes or dispatches |\n| `ingest_songwriter_name` | `import_songwriter_context` | Writes or dispatches |\n| `ingest_company_name` | `import_music_company_context` | Writes or dispatches |\n| `ingest_campaign_brief` | `import_music_campaign_brief` | Writes or dispatches |\n| `ingest_supporting_text` | `import_music_supporting_text` | Writes or dispatches |\n| `ingest_release` | `import_music_release_context` | Writes or dispatches |\n| `verify_release` | `verify_music_release` | Writes or dispatches |\n| `ingest` | `import_song_context` | Writes or dispatches |\n| `read` | `read_music_context` | Read-only hint |\n| `brief` | `compile_music_context_brief` | Read-only hint |", "security": [ { "ApiKey": [] }, { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "oneOf": [ { "type": "object", "properties": { "action": { "type": "string", "const": "list_release_cases" }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." }, "after_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" } }, "required": [ "action" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "read_release_case" }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." }, "request_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" } }, "required": [ "action", "request_id" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "read_release_case_review" }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." }, "review_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" } }, "required": [ "action", "review_id" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "review_release_case" }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." }, "request_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "fingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, "decision": { "type": "string", "enum": [ "reviewed", "needs_changes" ] }, "note": { "default": "", "type": "string", "maxLength": 2000 }, "idempotency_key": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9._:-]+$" } }, "required": [ "action", "request_id", "fingerprint", "decision", "idempotency_key" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "save_brief" }, "request_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "additional_request_ids": { "default": [], "maxItems": 9, "type": "array", "items": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "description": "Up to nine additional requests in the same workspace. Every selected request must be completed or partial. Duplicate IDs are read once." }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." }, "purpose": { "type": "string", "enum": [ "creative_direction", "playlist_pitch", "company_onboarding" ] }, "max_characters": { "default": 12000, "type": "integer", "minimum": 1000, "maximum": 32000, "description": "Bounds selected evidence JSON and rendered Markdown independently, not the entire response. Whole documents are omitted to fit; inspect reported gaps." }, "idempotency_key": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9._:-]+$", "description": "Freezes the server-compiled output. Reusing the key with different output conflicts; use a new key for a newer snapshot." } }, "required": [ "action", "request_id", "purpose", "idempotency_key" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "read_brief" }, "brief_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." } }, "required": [ "action", "brief_id" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "plan" }, "request_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." } }, "required": [ "action", "request_id" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "list_executions" }, "request_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." } }, "required": [ "action", "request_id" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "list_catalog_members" }, "request_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "subject_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." }, "after_isrc": { "type": "string", "minLength": 1, "maxLength": 100 }, "limit": { "default": 100, "type": "integer", "minimum": 1, "maximum": 100 } }, "required": [ "action", "request_id", "subject_id" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "list_release_tracks" }, "request_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "subject_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." }, "after_slot": { "default": -1, "type": "integer", "minimum": -1, "maximum": 9007199254740991 }, "limit": { "default": 100, "type": "integer", "minimum": 1, "maximum": 100 } }, "required": [ "action", "request_id", "subject_id" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "read_release_track_observations" }, "request_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "subject_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." } }, "required": [ "action", "request_id", "subject_id" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "review_release_track_identities" }, "request_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "subject_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." } }, "required": [ "action", "request_id", "subject_id" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "verify_release_tracks" }, "request_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "subject_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." } }, "required": [ "action", "request_id", "subject_id" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "expand_catalog_members" }, "request_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "subject_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." }, "after_isrc": { "type": "string", "minLength": 1, "maxLength": 100 }, "limit": { "default": 100, "type": "integer", "minimum": 1, "maximum": 100 } }, "required": [ "action", "request_id", "subject_id" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "read_execution" }, "execution_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." } }, "required": [ "action", "execution_id" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "ingest_catalog" }, "catalog_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." }, "idempotency_key": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9._:-]+$" } }, "required": [ "action", "catalog_id", "idempotency_key" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "ingest_artist" }, "artist_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." }, "idempotency_key": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9._:-]+$" } }, "required": [ "action", "artist_id", "idempotency_key" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "ingest_songwriter_name" }, "name": { "type": "string", "minLength": 2, "maxLength": 200 }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." }, "idempotency_key": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9._:-]+$" } }, "required": [ "action", "name", "idempotency_key" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "ingest_company_name" }, "name": { "type": "string", "minLength": 2, "maxLength": 200 }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." }, "idempotency_key": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9._:-]+$" } }, "required": [ "action", "name", "idempotency_key" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "ingest_campaign_brief" }, "brief": { "type": "object", "properties": { "name": { "type": "string", "minLength": 2, "maxLength": 200 }, "goal": { "type": "string", "minLength": 2, "maxLength": 2000 }, "audience": { "type": "string", "maxLength": 500 }, "start_date": { "type": "string", "format": "date", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$" }, "end_date": { "type": "string", "format": "date", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$" } }, "required": [ "name", "goal" ], "additionalProperties": false, "description": "Customer-provided campaign direction; end_date must not precede start_date. This is an assertion, not verified external evidence." }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." }, "idempotency_key": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9._:-]+$" } }, "required": [ "action", "brief", "idempotency_key" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "ingest_supporting_text" }, "title": { "type": "string", "minLength": 2, "maxLength": 200 }, "text": { "type": "string", "minLength": 1, "maxLength": 20000 }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." }, "idempotency_key": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9._:-]+$" } }, "required": [ "action", "title", "text", "idempotency_key" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "ingest_release" }, "url": { "type": "string", "maxLength": 2048, "format": "uri", "description": "Use a public HTTPS open.spotify.com album URL, including a single release album page. Other hosts and resource types fail the operation with 409." }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." }, "idempotency_key": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9._:-]+$" } }, "required": [ "action", "url", "idempotency_key" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "verify_release" }, "request_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." } }, "required": [ "action", "request_id" ], "additionalProperties": false }, { "type": "object", "properties": { "url": { "type": "string", "maxLength": 2048, "format": "uri", "description": "Use a public HTTPS Spotify track URL. The URL parser also recognizes a single YouTube video, but the pilot ingestion operation rejects YouTube with 409. Albums use ingest_release." }, "idempotency_key": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9._:-]+$" }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." }, "topics": { "minItems": 1, "maxItems": 10, "type": "array", "items": { "type": "string", "enum": [ "release_metadata", "artist_metadata", "catalog_metadata", "lyrics", "song_summary", "artwork_branding", "artist_research", "artist_brand", "era", "video_narrative" ] } }, "direction": { "type": "string", "maxLength": 4000 }, "action": { "type": "string", "const": "ingest" } }, "required": [ "url", "idempotency_key", "action" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "read" }, "request_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." } }, "required": [ "action", "request_id" ], "additionalProperties": false }, { "type": "object", "properties": { "action": { "type": "string", "const": "brief" }, "request_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "additional_request_ids": { "default": [], "maxItems": 9, "type": "array", "items": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "description": "Up to nine additional requests in the same workspace. Every selected request must be completed or partial. Duplicate IDs are read once." }, "organization_id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "Optional organization workspace. Current membership and ownership are checked; omitting it uses personal scope." }, "purpose": { "type": "string", "enum": [ "creative_direction", "playlist_pitch", "company_onboarding" ] }, "max_characters": { "default": 12000, "type": "integer", "minimum": 1000, "maximum": 32000, "description": "Bounds selected evidence JSON and rendered Markdown independently, not the entire response. Whole documents are omitted to fit; inspect reported gaps." } }, "required": [ "action", "request_id", "purpose" ], "additionalProperties": false } ] }, "examples": { "twoSongBrief": { "summary": "Compile creative direction from two saved songs", "value": { "action": "brief", "request_id": "11111111-1111-4111-8111-111111111111", "additional_request_ids": [ "22222222-2222-4222-8222-222222222222" ], "purpose": "creative_direction", "max_characters": 12000 } }, "saveBrief": { "summary": "Save a playlist-pitch brief", "value": { "action": "save_brief", "request_id": "11111111-1111-4111-8111-111111111111", "purpose": "playlist_pitch", "idempotency_key": "pitch-v1" } }, "readBrief": { "summary": "Read an exact saved brief", "value": { "action": "read_brief", "brief_id": "33333333-3333-4333-8333-333333333333" } } } } } }, "responses": { "200": { "description": "Action-specific result: request, page, execution(s), observation, review, saved snapshot, release-case result, module plan, or a compiled brief. Inspect readiness and coverage; 200 does not establish complete evidence.\n\nRead returns request. Brief returns request_id, request_ids, purpose, readiness, bounded Markdown text and text_characters, documents and their serialized characters count, missingTopics, guidance, gaps, request_coverage, and input_manifest. Each coverage entry includes requestId, readiness and missingTopics; overall readiness remains partial if any request has a gap or partial evidence. The manifest includes compilerVersion, method, requestIds, excludedTopics, and selected documents with documentId, resultId (null when not recorded), subjectId, topic, version and sourceVersionIds. The result is returned without creating a saved brief record. Save/read brief return {snapshot: {id, created_at, purpose, state, superseded, brief}}. state is saved or unavailable; unavailable snapshots return brief: null. saved snapshots return the exact compiled response including its input manifest. Another workspace cannot read the record. Company-onboarding briefs also include assessment_scope and next_steps. Other action groups return the action-specific result described above; a successful response does not establish complete coverage.", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "202": { "description": "Accepted ingest, verify_release, or verify_release_tracks. Ingest returns request; verification returns request_id and verificationQueued, or request_id, subject_id and lookupQueued. Acceptance does not mean completion.", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "400": { "description": "Invalid input" }, "401": { "description": "Authentication required" }, "403": { "description": "The authenticated account cannot access the selected organization workspace." }, "409": { "description": "Operation failed, access unavailable, input/key conflict, or request not found. Retry ingestion with the same idempotency key after correcting the cause." } }, "x-undocumented-actions": [ "plan_catalog_members" ], "x-recoup-mcp-operations": { "list_release_cases": { "name": "list_music_release_cases", "readOnly": true }, "read_release_case": { "name": "read_music_release_case", "readOnly": true }, "read_release_case_review": { "name": "read_music_release_case_review", "readOnly": true }, "review_release_case": { "name": "record_music_release_case_review", "readOnly": false }, "save_brief": { "name": "save_music_context_brief", "readOnly": false }, "read_brief": { "name": "read_music_context_brief", "readOnly": true }, "plan": { "name": "plan_music_context_research", "readOnly": true }, "list_executions": { "name": "list_music_context_executions", "readOnly": true }, "list_catalog_members": { "name": "list_music_context_catalog_members", "readOnly": true }, "list_release_tracks": { "name": "list_music_context_release_tracks", "readOnly": true }, "read_release_track_observations": { "name": "read_music_release_track_observations", "readOnly": true }, "review_release_track_identities": { "name": "review_music_release_track_identities", "readOnly": true }, "verify_release_tracks": { "name": "verify_music_release_tracks", "readOnly": false }, "expand_catalog_members": { "name": "expand_music_context_catalog", "readOnly": false }, "read_execution": { "name": "read_music_context_execution", "readOnly": true }, "ingest_catalog": { "name": "import_catalog_context", "readOnly": false }, "ingest_artist": { "name": "import_artist_context", "readOnly": false }, "ingest_songwriter_name": { "name": "import_songwriter_context", "readOnly": false }, "ingest_company_name": { "name": "import_music_company_context", "readOnly": false }, "ingest_campaign_brief": { "name": "import_music_campaign_brief", "readOnly": false }, "ingest_supporting_text": { "name": "import_music_supporting_text", "readOnly": false }, "ingest_release": { "name": "import_music_release_context", "readOnly": false }, "verify_release": { "name": "verify_music_release", "readOnly": false }, "ingest": { "name": "import_song_context", "readOnly": false }, "read": { "name": "read_music_context", "readOnly": true }, "brief": { "name": "compile_music_context_brief", "readOnly": true } } } } }, "components": { "securitySchemes": { "ApiKey": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "BearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Start guest context Source: https://recoupable.dev/docs/api-reference/context/guest-start Start guest context extraction ## POST /api/context/guest Full OpenAPI specification: https://recoupable.dev/docs/spec/context.json ## Authentication This operation's specification permits a request without authentication. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup Context Engine", "version": "0.1.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/context/guest": { "post": { "summary": "Start guest context extraction", "description": "Feature-flagged metadata pilot. Saves a Spotify track URL in a temporary workspace before signup. Uses an HttpOnly seven-day cookie; only its hash is stored. One URL per guest session and 100 new guest workspaces per day globally. Extracts artist and release metadata only. Does not run paid enrichment, generate a site, or send email. Serve through the same-origin funnel; cross-site cookie integration is not provided. Retry the same URL and cookie to resume dispatch.", "security": [], "parameters": [ { "name": "Origin", "in": "header", "required": true, "schema": { "type": "string" }, "description": "The configured funnel origin. This is a browser session flow, not a public unauthenticated MCP tool." } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": false, "required": [ "action", "url" ], "properties": { "action": { "type": "string", "enum": [ "start" ] }, "url": { "type": "string", "format": "uri" } } } } } }, "responses": { "200": { "description": "Saved guest context or claim receipt." }, "202": { "description": "Guest work saved and workflow dispatched. Preserve Set-Cookie for status and claim." }, "400": { "description": "Invalid request." }, "401": { "description": "Guest session or account authentication missing." }, "403": { "description": "Origin or workspace access denied." }, "409": { "description": "Operation unavailable; retry the same input and session." }, "503": { "description": "Guest context is disabled." } } } } } } ``` # Read guest context Source: https://recoupable.dev/docs/api-reference/context/guest-read Read guest context ## GET /api/context/guest Full OpenAPI specification: https://recoupable.dev/docs/spec/context.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup Context Engine", "version": "0.1.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/context/guest": { "get": { "summary": "Read guest context", "description": "Returns metadata and status for the current unclaimed, unexpired guest session. Guest access ends when claimed. Unclaimed work expires after seven days.", "security": [ { "GuestSession": [] } ], "responses": { "200": { "description": "Saved guest context or claim receipt." }, "400": { "description": "Invalid request." }, "401": { "description": "Guest session or account authentication missing." }, "403": { "description": "Origin or workspace access denied." }, "409": { "description": "Operation unavailable; retry the same input and session." }, "503": { "description": "Guest context is disabled." } } } } }, "components": { "securitySchemes": { "GuestSession": { "type": "apiKey", "in": "cookie", "name": "recoup_context_guest" } } } } ``` # Claim guest context Source: https://recoupable.dev/docs/api-reference/context/guest-claim Claim guest context after sign-in ## POST /api/context/guest/claim Full OpenAPI specification: https://recoupable.dev/docs/spec/context.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup Context Engine", "version": "0.1.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/context/guest/claim": { "post": { "summary": "Claim guest context after sign-in", "description": "Requires the original guest cookie plus verified account authentication. Optionally claims into an authorized organization. Transfers saved metadata without refetching it; an in-progress worker finishes into the claimed destination. Repeated claims by the same account and destination return the same request ID. Another account cannot claim it. Existing matching identities and customer corrections are preserved. No artist ownership claim is implied. Signup UI integration must call this endpoint after authentication. The Recoup app provides a /context entry page and claims into the personal account after login. A failed extraction remains retryable through the same claim receipt. No completion email or website is generated by this flow.", "security": [ { "GuestSession": [], "BearerAuth": [] }, { "GuestSession": [], "ApiKey": [] } ], "parameters": [ { "name": "Origin", "in": "header", "required": true, "schema": { "type": "string" }, "description": "The configured funnel origin. This is a browser session flow, not a public unauthenticated MCP tool." } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": false, "required": [ "action" ], "properties": { "action": { "type": "string", "enum": [ "claim" ] }, "organization_id": { "type": "string", "format": "uuid" } } } } } }, "responses": { "200": { "description": "Saved guest context or claim receipt." }, "400": { "description": "Invalid request." }, "401": { "description": "Guest session or account authentication missing." }, "403": { "description": "Origin or workspace access denied." }, "409": { "description": "Operation unavailable; retry the same input and session." }, "503": { "description": "Guest context is disabled." } } } } }, "components": { "securitySchemes": { "GuestSession": { "type": "apiKey", "in": "cookie", "name": "recoup_context_guest" }, "BearerAuth": { "type": "http", "scheme": "bearer" }, "ApiKey": { "type": "apiKey", "in": "header", "name": "x-api-key" } } } } ``` # Generate a Music Video Source: https://recoupable.dev/docs/workflows/generate-music-video End-to-end workflow to produce a vertical, social-ready music video — image, motion, caption — from an artist and a song. This is the canonical recipe used internally by Recoup's `create-content` background task. Two paths are documented below: the **async pipeline** that an LLM agent should use, and the **manual recipe** for humans (or for cases where you want to swap a single step). ## Hand it to your LLM The fastest way to use this guide is to point an LLM at it directly — every page on this site is also served as raw markdown at the same path with a `.md` suffix, so models can fetch and follow it without parsing HTML: ``` Generate a music video for The Weeknd – "Blinding Lights" using https://docs.recoupable.dev/workflows/generate-music-video.md ``` Swap in your own artist and song. The LLM will resolve the artist, kick off the async pipeline, poll until done, and pull back the final video URL. ## Running as an agent? Use the async pipeline `POST /api/content/video` is synchronous and routinely takes 60–180s. Most agent shells (Claude Cowork, OpenAI tool calls, etc.) cap a single command at 30–60s and kill background processes when the shell exits — the manual recipe below is effectively un-runnable from those environments. Use the async path instead — same five steps, run server-side: ```bash # Trigger RUN_IDS=$(curl -sS -X POST "https://api.recoupable.dev/api/content/create" \ -H "x-api-key: $RECOUP_API_KEY" -H "Content-Type: application/json" \ -d "$(jq -n --arg artist "$ARTIST_ACCOUNT_ID" --arg template "$TEMPLATE" \ '{artist_account_id: $artist, template: $template}')" \ | jq -r '.runIds[]') # Poll (every ~10s) until COMPLETED / FAILED / CANCELED / CRASHED RUN_ID=$(echo "$RUN_IDS" | head -1) until STATUS=$(curl -sS "https://api.recoupable.dev/api/tasks/runs?runId=$RUN_ID" \ -H "x-api-key: $RECOUP_API_KEY" \ | jq -r '.runs[0].status') && \ [[ "$STATUS" =~ ^(COMPLETED|FAILED|CANCELED|CRASHED)$ ]]; do sleep 10 done # Read the output curl -sS "https://api.recoupable.dev/api/tasks/runs?runId=$RUN_ID" \ -H "x-api-key: $RECOUP_API_KEY" \ | jq '.runs[0].output' # -> { videoSourceUrl, imageUrl, captionText, template, lipsync, audio: {...} } ``` Polling fits inside short shell timeouts and survives session restarts. See [Tasks Runs](https://recoupable.dev/docs/api-reference/tasks/runs) for the full status enum (`QUEUED`, `EXECUTING`, `COMPLETED`, `FAILED`, `CANCELED`, `CRASHED`, etc.) and the `CreateContentRunOutput` schema. ### Resolving `$ARTIST_ACCOUNT_ID` `POST /api/content/create` needs the artist's `account_id`. Three calls: ```bash ORG_ID=$(curl -sS "https://api.recoupable.dev/api/organizations" \ -H "x-api-key: $RECOUP_API_KEY" | jq -r '.organizations[0].id') ARTIST_ACCOUNT_ID=$(curl -sS "https://api.recoupable.dev/api/artists?org_id=$ORG_ID" \ -H "x-api-key: $RECOUP_API_KEY" \ | jq -r --arg name "$ARTIST_NAME" '.artists[] | select(.name == $name) | .account_id') ``` The artist record exposes both `id` and `account_id` (both UUIDs). Use **`account_id`** — `id` is the artist row's primary key, `account_id` is the underlying account that owns it. The two are easy to swap; you'll get a 404 from `/api/content/create` if you pass the wrong one. ## Where the song lives Step 5 (and the async pipeline's lipsync mode) need a `song.mp3`. **Don't assume one exists, and don't assume the user has one locally.** Walk the agent through this fallback chain: 1. **Check the artist's sandbox repo first.** Each Recoup account has a backing GitHub repo. If the user has imported songs through Recoup, they live at predictable paths: ``` .openclaw/workspace/orgs/{org-slug}/artists/{artist-slug}/songs/{song-slug}/{song-slug}.mp3 /lyrics.json /clips.json ``` Discover the repo with [`GET /api/sandboxes`](https://recoupable.dev/docs/api-reference/sandboxes/list) (returns `github_repo` and a `filetree`); fetch a file with [`GET /api/sandboxes/file?path=…`](https://recoupable.dev/docs/api-reference/sandboxes/get-file). **Binary files (`.mp3`, `.png`, `.mp4`) come back base64-encoded in the `content` field — decode before writing to disk.** 2. **If no song is in the sandbox, ask the user how to proceed.** Two options to offer: - *"Want me to fetch the audio from YouTube?"* — agent downloads via `yt-dlp` (or equivalent), saves locally; user is responsible for any rights / DSP-licensing implications. - *"Want to supply the song yourself?"* — user uploads / drops a path; agent reads from there. Don't pick a path silently. The cost of fetching the wrong song from YouTube (or fetching one at all) is enough that the user should make the call. 3. **Don't fall back to "use a placeholder track."** A music video without the song is not a deliverable. ## Manual recipe (humans + targeted overrides) The rest of this page walks the same steps you can run by hand or call individually if you want to swap a single stage (different prompt for image, different motion, different caption length). ### Prerequisites - An auth credential for `api.recoupable.dev`. Two options — pick one and use it for every call below: - **API key** (`recoup_sk_…`, recommended for sandbox / agent use): pass as `-H "x-api-key: $RECOUP_API_KEY"`. - One-shot agent: `POST /api/agents/signup` with an `agent+{unique}@recoupable.com` email returns the key immediately. - Real-email signup: same endpoint with a real email mails a 6-digit code; complete with `POST /api/agents/verify`. See [Agents](https://recoupable.dev/docs/agents). - **Privy access token** (for end-user flows in chat/UI): pass as `-H "Authorization: Bearer $RECOUP_ACCESS_TOKEN"`. - The examples below use `x-api-key`. Substitute `Authorization: Bearer …` if you're using a Privy token. - `$ARTIST_NAME`, `$SONG_TITLE`, `$SONG_LYRICS_CLIP` (a 1–2 sentence mood snippet) - `$REFERENCE_IMAGE_URL` *(optional)* — an artist photo or album cover to seed the image; if your template's purpose is "show this exact image" (e.g. `album-record-store`), set this and skip image generation in step 2 - A `song.mp3` for step 5. **Don't ask the user for a local file** — fetch from the artist's repo via `/api/sandboxes/file`. - `ffmpeg` installed locally for step 5 ### Step 0: Scaffold the workspace BEFORE any API call The `VIDEO.md` checklist *is* the workflow state — tick boxes and persist values back to the frontmatter as you go. To resume later, find the first unchecked box. ```bash VIDEO_SLUG=$(echo "$SONG_TITLE" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-//; s/-$//') VIDEO_DIR="videos/$VIDEO_SLUG" mkdir -p "$VIDEO_DIR" cat > "$VIDEO_DIR/VIDEO.md" < "$VIDEO_DIR/caption.txt" # Single-line filter graph — newlines inside -filter_complex are literal characters and break the [v] label ffmpeg -y \ -stream_loop -1 -i "$VIDEO_DIR/clip.mp4" \ -i "$SONG_PATH" \ -filter_complex "[0:v]crop=ih*9/16:ih,scale=1080:1920,drawtext=fontfile=$CAPTION_FONT:textfile=$VIDEO_DIR/caption.txt:fontcolor=$CAPTION_COLOR:fontsize=$CAPTION_FONT_SIZE:x=(w-text_w)/2:y=h-text_h-120:box=1:boxcolor=$CAPTION_STROKE@0.5:boxborderw=20[v]" \ -map "[v]" -map "1:a" \ -shortest -c:v libx264 -c:a aac -pix_fmt yuv420p \ "$FINAL_PATH" ``` **After:** write `finalVideoPath`, tick the box. With every box ticked, the music video is complete. ## Step 6: Publish (optional) Once the MP4 is rendered, push it to the artist's socials with the [Connectors API](https://recoupable.dev/docs/api-reference/connectors/list). One heads-up worth knowing: - **TikTok URL ownership.** `TIKTOK_PUBLISH_VIDEO` (pull-from-URL mode) requires the source domain be verified in the TikTok dev portal. `fal.media` URLs will fail with `url_ownership_unverified` — use `TIKTOK_UPLOAD_VIDEO` instead, which accepts the same URL and uploads server-side. --- The checklist is the source of truth — if a box isn't ticked, treat the step as not run. # Create Content Source: https://recoupable.dev/docs/api-reference/content/create Trigger the content creation pipeline for an artist. Provide `artist_account_id` to identify the target artist. Validates the artist has all required files (face guide, songs) unless overridden via `songs` URLs or `images`, then triggers a background task that generates a short-form video. Returns `runIds` — an array of run IDs that can each be polled via [GET /api/tasks/runs](/docs/api-reference/tasks/runs). ## POST /api/content/create Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/content/create": { "post": { "description": "Trigger the content creation pipeline for an artist. Provide `artist_account_id` to identify the target artist. Validates the artist has all required files (face guide, songs) unless overridden via `songs` URLs or `images`, then triggers a background task that generates a short-form video. Returns `runIds` — an array of run IDs that can each be polled via [GET /api/tasks/runs](/docs/api-reference/tasks/runs).", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Content creation parameters including the target artist and optional template/workflow settings", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentCreateRequest" } } } }, "responses": { "202": { "description": "Pipeline triggered successfully. Returns `runIds` — an array of run IDs. Poll each via [GET /api/tasks/runs](/docs/api-reference/tasks/runs) to check progress.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentCreateResponse" } } } }, "400": { "description": "Validation failed — missing artist identifier, artist is missing required files, or template not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentCreateErrorResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "404": { "description": "Artist not found — the provided artist_account_id does not match any artist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } } } } } }, "components": { "schemas": { "ContentCreateRequest": { "type": "object", "description": "Parameters for triggering the content creation pipeline.", "properties": { "artist_account_id": { "type": "string", "format": "uuid", "description": "UUID of the artist account to create content for. Use [GET /api/artists](/docs/api-reference/artists/list) to find artist account IDs.", "example": "1873859c-dd37-4e9a-9bac-80d3558527a9" }, "template": { "type": "string", "description": "Optional template ID for content generation. Defines the visual style, scene, and prompt configuration. When omitted, the pipeline runs in malleable mode using only the params you provide. See GET /api/content/templates for available options.", "example": "artist-caption-stage" }, "lipsync": { "type": "boolean", "description": "Whether to generate video with lip-synced audio. When `true`, uses an audio-to-video model that bakes audio into the video for lip movement. When `false`, generates video from the image alone and overlays audio in post. If omitted, the template's default workflow is used.", "example": false }, "caption_length": { "type": "string", "enum": [ "none", "short", "medium", "long" ], "description": "Controls the length of the generated caption text. `none` skips caption generation. `short` produces 1-2 lines (punchy, minimal). `medium` produces 1-2 sentences. `long` produces a paragraph (stream of consciousness style). Defaults to `none`.", "default": "none", "example": "none" }, "upscale": { "type": "boolean", "description": "Whether to upscale the generated image and video for higher resolution and detail. Adds approximately 2 minutes to the pipeline. Defaults to `false`.", "default": false, "example": false }, "batch": { "type": "integer", "minimum": 1, "maximum": 30, "description": "Number of videos to generate in parallel. Each video independently selects a random reference image, song clip, and mood variation. The response always returns `runIds` as an array. Defaults to `1`.", "default": 1, "example": 1 }, "songs": { "type": "array", "items": { "type": "string" }, "description": "Optional list of song slugs or public URLs to use for the audio track. Song slugs match filenames without extension from the artist's `songs/` directory (e.g. `\"hiccups\"` for `hiccups.mp3`). Public URLs (e.g. `\"https://example.com/my-song.mp3\"`) are downloaded, transcribed, and clipped directly — bypassing the Git repo. When omitted, all songs in the artist's repo are eligible.", "example": [ "hiccups", "https://example.com/unreleased-track.mp3" ] }, "images": { "type": "array", "items": { "type": "string", "format": "uri" }, "description": "Optional list of public image URLs to use as face guides instead of the artist's default `face-guide.png` from their GitHub repo. The first image is used as the primary face guide. Useful when the caller wants to override the default face reference.", "example": [ "https://example.com/face.png" ] } }, "required": [ "artist_account_id" ] }, "ContentCreateResponse": { "type": "object", "required": [ "runIds", "status", "artist_account_id" ], "description": "Confirmation that the content creation pipeline has been triggered. Always returns `runIds` as an array — even for a single run, it contains one element.", "properties": { "runIds": { "type": "array", "items": { "type": "string" }, "description": "Array of run IDs. Poll each via [GET /api/tasks/runs](/docs/api-reference/tasks/runs). For single runs this contains one element.", "example": [ "run_abc123def456" ] }, "status": { "type": "string", "enum": [ "triggered" ], "description": "Indicates the pipeline has been triggered" }, "artist_account_id": { "type": "string", "format": "uuid", "description": "UUID of the artist account the pipeline is running for", "example": "1873859c-dd37-4e9a-9bac-80d3558527a9" }, "template": { "type": [ "string", "null" ], "description": "Template ID when a preset pipeline is used; null in malleable mode.", "example": null }, "lipsync": { "type": "boolean", "description": "Whether lip-sync mode is enabled", "example": false }, "failed": { "type": "integer", "description": "Number of triggers that failed. Only present when some triggers failed.", "example": 0 } } }, "ContentCreateErrorResponse": { "type": "object", "required": [ "error" ], "description": "Returned when the artist is missing required files or the template is not found. Includes actionable instructions for resolving each issue.", "properties": { "error": { "type": "string", "description": "Human-readable error summary", "example": "Artist 'new-artist' is not ready for content creation" }, "ready": { "type": "boolean", "description": "Always `false` when this error is returned", "example": false }, "missing": { "type": "array", "description": "List of missing files with severity and fix instructions. Only present when the artist fails validation.", "items": { "$ref": "#/components/schemas/ContentMissingFile" } }, "available_templates": { "type": "array", "description": "List of valid template names. Only present when the requested template was not found.", "items": { "type": "string" }, "example": [ "artist-caption-bedroom", "artist-caption-outside", "artist-caption-stage" ] } } }, "ContentErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Unauthorized" } } }, "ContentMissingFile": { "type": "object", "required": [ "file", "severity", "description", "fix" ], "description": "A missing file that prevents or degrades content creation.", "properties": { "file": { "type": "string", "description": "Relative path of the missing file within the artist directory", "example": "context/images/face-guide.png" }, "severity": { "type": "string", "enum": [ "required", "recommended" ], "description": "`required` means the pipeline will fail without this file. `recommended` means the pipeline will run but output quality is degraded.", "example": "required" }, "description": { "type": "string", "description": "What this file is used for in the pipeline", "example": "Face guide image used for AI image generation" }, "fix": { "type": "string", "description": "Actionable instructions for creating or adding the missing file", "example": "Generate a face guide using fal-ai/nano-banana-pro/edit with 2-3 reference photos of the artist" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Generate Image Source: https://recoupable.dev/docs/api-reference/content/generate-image Generate an image from a text prompt. Pass `image_urls` to edit those images with the prompt instead of generating a new one. Returns the image URL. ## POST /api/content/image Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/content/image": { "post": { "description": "Generate an image from a text prompt. Pass `image_urls` to edit those images with the prompt instead of generating a new one. Returns the image URL.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Image generation parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentCreateImageRequest" } } } }, "responses": { "200": { "description": "Image generated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentCreateImageResponse" } } } }, "400": { "description": "Validation failed — invalid or missing request body fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "402": { "description": "Insufficient credits. Charged per generated image at the pinned model's rate ($0.01 per image). A free-tier account without sufficient credits receives a 402.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } } } } } }, "components": { "schemas": { "ContentCreateImageRequest": { "type": "object", "properties": { "prompt": { "type": "string", "description": "Text prompt used to generate or edit the image" }, "image_urls": { "type": "array", "items": { "type": "string", "format": "uri" }, "minItems": 1, "maxItems": 10, "description": "Reference images to edit (1-10 HTTP(S) URLs). When provided, edits these images with `prompt` instead of generating a new one." }, "num_images": { "type": "integer", "minimum": 1, "maximum": 4, "default": 1, "description": "Number of images to generate (1-4)" }, "aspect_ratio": { "type": "string", "enum": [ "21:9", "16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16", "9:21" ], "description": "Output aspect ratio. If omitted, the model chooses automatically based on the request." }, "output_format": { "type": "string", "enum": [ "jpeg", "png", "webp" ], "default": "webp", "description": "Format of the generated image" }, "sync_mode": { "type": "boolean", "default": false, "description": "If true, the image is returned as a data URI and is not persisted in the request history" }, "reference_image_url": { "type": "string", "format": "uri", "deprecated": true, "description": "Compatibility alias translated into image_urls. Combined unique reference list is limited to 10. New clients must use image_urls." }, "images": { "type": "array", "items": { "type": "string", "format": "uri" }, "deprecated": true, "description": "Compatibility alias translated into image_urls. Combined unique reference list is limited to 10. New clients must use image_urls." } }, "additionalProperties": false, "example": { "prompt": "Edit the lighting while preserving this artist.", "image_urls": [ "https://example.com/artist.png" ], "num_images": 1 }, "description": "Generate images from a prompt, or edit the supplied image_urls. num_images controls the number of returned images. Unsupported request fields return 400 before paid generation." }, "ContentCreateImageResponse": { "type": "object", "required": [ "imageUrl", "images" ], "properties": { "imageUrl": { "type": "string", "format": "uri", "description": "URL of the first generated image" }, "images": { "type": "array", "items": { "type": "string", "format": "uri" }, "description": "URLs of all generated images. Contains one element when num_images is 1." } } }, "ContentErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Unauthorized" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Generate Video Source: https://recoupable.dev/docs/api-reference/content/generate-video Generate a video from a text prompt, optionally animating a still image (`image_url`) or transitioning between two images (`image_url` + `end_image_url`). ## POST /api/content/video Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/content/video": { "post": { "description": "Generate a video from a text prompt, optionally animating a still image (`image_url`) or transitioning between two images (`image_url` + `end_image_url`).", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Video generation parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentCreateVideoRequest" } } } }, "responses": { "200": { "description": "Video generated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentCreateVideoResponse" } } } }, "400": { "description": "Validation failed — invalid or missing request body fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "402": { "description": "Insufficient credits. Charged per second of output at the pinned model's rate ($0.08/s, MiniMax H3 Max). A free-tier account without sufficient credits receives a 402.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } } } } } }, "components": { "schemas": { "ContentCreateVideoRequest": { "type": "object", "required": [ "prompt" ], "properties": { "prompt": { "type": "string", "minLength": 1, "maxLength": 50000, "description": "Text prompt for video generation" }, "prompt_expansion_mode": { "type": "string", "default": "balanced", "enum": [ "balanced", "quality" ], "description": "How much effort to spend rewriting the prompt before generation. `balanced` returns in about a second. `quality` spends up to ~30s on a richer prompt." }, "image_url": { "type": "string", "format": "uri", "description": "URL of the image to use as the first frame. When provided, the output aspect ratio follows this image. Generate an image first via POST /api/content/image if needed." }, "end_image_url": { "type": "string", "format": "uri", "description": "Optional URL of the image to use as the last frame, for first-to-last keyframe generation" }, "duration": { "type": "integer", "minimum": 5, "maximum": 15, "default": 5, "description": "Duration of the generated video, in seconds" }, "resolution": { "type": "string", "enum": [ "480P", "768P" ], "default": "768P", "description": "The native generation resolution of the video" }, "seed": { "type": "integer", "description": "Random seed. A random seed is selected when omitted." }, "enable_safety_checker": { "type": "boolean", "default": true, "description": "If true, the safety checker will be enabled" }, "sync_mode": { "type": "boolean", "default": false, "description": "Return the generated video as base64 instead of a CDN URL" } }, "additionalProperties": false, "description": "House MiniMax H3 Max primitive. No mode, model, audio_url or video_url input. Unsupported semantic fields return 400 before paid generation. Audio-driven hosted presets use POST /api/content/create with lipsync true.", "example": { "prompt": "Slow camera push with natural movement.", "image_url": "https://example.com/artist.png", "duration": 15, "resolution": "768P" } }, "ContentCreateVideoResponse": { "type": "object", "required": [ "videoUrl" ], "properties": { "videoUrl": { "type": "string", "format": "uri", "description": "URL of the generated video" } } }, "ContentErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Unauthorized" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Generate Caption Source: https://recoupable.dev/docs/api-reference/content/generate-caption Generate a short caption from a topic. Returns the text content and default styling (font, color, size). ## POST /api/content/caption Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/content/caption": { "post": { "description": "Generate a short caption from a topic. Returns the text content and default styling (font, color, size).", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Text generation parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentCreateTextRequest" } } } }, "responses": { "200": { "description": "Text generated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentCreateTextResponse" } } } }, "400": { "description": "Validation failed — invalid or missing request body fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } } } } } }, "components": { "schemas": { "ContentCreateTextRequest": { "type": "object", "required": [ "topic" ], "properties": { "template": { "type": "string", "description": "Optional template ID. When provided, injects the template's caption guide (tone, rules, formats) and examples into the LLM prompt. Caller's topic is still required. See [GET /api/content/templates](/docs/api-reference/content/templates) for available options." }, "topic": { "type": "string", "description": "The subject or theme for caption generation" }, "length": { "type": "string", "enum": [ "short", "medium", "long" ], "default": "short", "description": "Desired text length" } } }, "ContentCreateTextResponse": { "type": "object", "required": [ "content", "color", "borderColor", "maxFontSize" ], "properties": { "content": { "type": "string", "description": "Generated on-screen text content" }, "font": { "type": [ "string", "null" ], "description": "Font name for the text, or null for default" }, "color": { "type": "string", "description": "Text color as a CSS color value", "example": "white" }, "borderColor": { "type": "string", "description": "Text border/stroke color as a CSS color value", "example": "black" }, "maxFontSize": { "type": "number", "description": "Maximum font size in pixels", "example": 42 } } }, "ContentErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Unauthorized" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Transcribe Audio Source: https://recoupable.dev/docs/api-reference/content/transcribe-audio Transcribe audio into text with word-level timestamps. Pass one or more audio file URLs in `audio_urls`. Returns the full transcript and an array of timed segments. ## POST /api/content/transcribe Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/content/transcribe": { "post": { "description": "Transcribe audio into text with word-level timestamps. Pass one or more audio file URLs in `audio_urls`. Returns the full transcript and an array of timed segments.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Audio transcription parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentCreateAudioRequest" } } } }, "responses": { "200": { "description": "Song transcribed successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentCreateAudioResponse" } } } }, "400": { "description": "Validation failed — invalid or missing request body fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } } } } } }, "components": { "schemas": { "ContentCreateAudioRequest": { "type": "object", "required": [ "audio_urls" ], "properties": { "audio_urls": { "type": "array", "items": { "type": "string", "format": "uri" }, "minItems": 1, "description": "Audio file URLs to transcribe" }, "language": { "type": "string", "default": "en", "description": "Language code for transcription (e.g. en, es, fr). Defaults to en." }, "chunk_level": { "type": "string", "enum": [ "none", "segment", "word" ], "default": "word", "description": "Granularity of timestamp chunks. Defaults to word-level." }, "diarize": { "type": "boolean", "default": false, "description": "Enable speaker diarization. Defaults to false." }, "model": { "type": "string", "description": "fal.ai model ID. Defaults to fal-ai/whisper" } } }, "ContentCreateAudioResponse": { "type": "object", "required": [ "audioUrl", "fullLyrics", "segments", "segmentCount" ], "properties": { "audioUrl": { "type": "string", "description": "URL of the transcribed audio" }, "fullLyrics": { "type": "string", "description": "Complete transcribed lyrics as a single string" }, "segments": { "type": "array", "items": { "$ref": "#/components/schemas/ContentCreateAudioSegment" }, "description": "Timestamped lyric segments" }, "segmentCount": { "type": "number", "description": "Total number of segments returned" } } }, "ContentErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Unauthorized" } } }, "ContentCreateAudioSegment": { "type": "object", "required": [ "start", "end", "text" ], "properties": { "start": { "type": "number", "description": "Segment start time in seconds" }, "end": { "type": "number", "description": "Segment end time in seconds" }, "text": { "type": "string", "description": "Transcribed text for this segment" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Edit Content Source: https://recoupable.dev/docs/api-reference/content/edit Apply ffmpeg edits to a video — trim, crop, resize, or overlay text. Pass a `template` for a preset edit pipeline, or build your own with an `operations` array. ## PATCH /api/content Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/content": { "patch": { "description": "Apply ffmpeg edits to a video — trim, crop, resize, or overlay text. Pass a `template` for a preset edit pipeline, or build your own with an `operations` array.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Edit parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentCreateEditRequest" } } } }, "responses": { "202": { "description": "Edit task triggered successfully. Poll via [GET /api/tasks/runs](/docs/api-reference/tasks/runs) using the returned `runId`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentCreateEditResponse" } } } }, "400": { "description": "Validation failed — invalid or missing request body fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } } } } } }, "components": { "schemas": { "ContentCreateEditRequest": { "type": "object", "required": [ "video_url" ], "description": "Requires a video URL. Operations are applied sequentially via ffmpeg.", "properties": { "video_url": { "type": "string", "format": "uri", "description": "Input video URL (required)" }, "template": { "type": "string", "description": "Template name for deterministic edit config. If provided, operations are read from the template. See [GET /api/content/templates](/docs/api-reference/content/templates) for available options." }, "operations": { "type": "array", "description": "Array of edit operations to apply in order. Required if template is not provided.", "items": { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "trim", "crop", "resize", "overlay_text" ], "description": "Operation type" }, "start": { "type": "number", "description": "Start time in seconds (trim)" }, "duration": { "type": "number", "description": "Duration in seconds (trim)" }, "aspect": { "type": "string", "description": "Aspect ratio e.g. 9:16 (crop)" }, "width": { "type": "integer", "description": "Width in pixels (crop/resize)" }, "height": { "type": "integer", "description": "Height in pixels (crop/resize)" }, "content": { "type": "string", "description": "Text content (overlay_text)" }, "font": { "type": "string", "description": "Font name (overlay_text)" }, "color": { "type": "string", "description": "Text color (overlay_text)" }, "stroke_color": { "type": "string", "description": "Text stroke color (overlay_text)" }, "max_font_size": { "type": "number", "description": "Maximum font size (overlay_text)" }, "position": { "type": "string", "enum": [ "top", "center", "bottom" ], "description": "Text position (overlay_text)" } } } }, "output_format": { "type": "string", "enum": [ "mp4", "webm", "mov" ], "default": "mp4", "description": "Output format" } } }, "ContentCreateEditResponse": { "type": "object", "required": [ "runId", "status" ], "properties": { "runId": { "type": "string", "description": "Background task run ID. Poll via [GET /api/tasks/runs](/docs/api-reference/tasks/runs) to check progress." }, "status": { "type": "string", "enum": [ "triggered" ], "description": "Status of the edit task" } } }, "ContentErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Unauthorized" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Upscale Content Source: https://recoupable.dev/docs/api-reference/content/upscale Upscale an image or video to higher resolution. Pass the URL and specify the type. Returns the upscaled URL. ## POST /api/content/upscale Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/content/upscale": { "post": { "description": "Upscale an image or video to higher resolution. Pass the URL and specify the type. Returns the upscaled URL.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Upscale parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentCreateUpscaleRequest" } } } }, "responses": { "200": { "description": "Media upscaled successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentCreateUpscaleResponse" } } } }, "400": { "description": "Validation failed — invalid or missing request body fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } } } } } }, "components": { "schemas": { "ContentCreateUpscaleRequest": { "type": "object", "required": [ "url", "type" ], "properties": { "url": { "type": "string", "format": "uri", "description": "URL of the image or video to upscale" }, "type": { "type": "string", "enum": [ "image", "video" ], "description": "Whether the input is an image or video" }, "upscale_factor": { "type": "number", "minimum": 1, "maximum": 4, "default": 2, "description": "Upscale multiplier (1-4x). Defaults to 2x." }, "target_resolution": { "type": "string", "enum": [ "720p", "1080p", "1440p", "2160p" ], "description": "Target output resolution. When provided, overrides upscale_factor with a resolution-based upscale." } } }, "ContentCreateUpscaleResponse": { "type": "object", "required": [ "url" ], "properties": { "url": { "type": "string", "format": "uri", "description": "URL of the upscaled image or video" } } }, "ContentErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Unauthorized" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Analyze Video Source: https://recoupable.dev/docs/api-reference/content/analyze-video Analyze a video and answer questions about it. Pass a video URL and a text prompt — for example, "Describe what happens" or "Rate the visual quality 1-10." Returns the generated text. ## POST /api/content/analyze Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/content/analyze": { "post": { "description": "Analyze a video and answer questions about it. Pass a video URL and a text prompt — for example, \"Describe what happens\" or \"Rate the visual quality 1-10.\" Returns the generated text.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Video analysis parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentCreateAnalyzeRequest" } } } }, "responses": { "200": { "description": "Video analyzed successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentCreateAnalyzeResponse" } } } }, "400": { "description": "Validation failed — invalid or missing request body fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "502": { "description": "Video analysis failed upstream", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } } } } } }, "components": { "schemas": { "ContentCreateAnalyzeRequest": { "type": "object", "required": [ "video_url", "prompt" ], "properties": { "video_url": { "type": "string", "format": "uri", "description": "Publicly accessible URL of the video to analyze. Supported formats: MP4, MOV, AVI, and other FFmpeg-compatible formats. Maximum duration: 1 hour." }, "prompt": { "type": "string", "maxLength": 2000, "description": "A text prompt that guides the analysis. Can be instructive, descriptive, or phrased as a question. Examples: \"Describe the key moments in this video\", \"Generate 5 SEO keywords for this video\"." }, "temperature": { "type": "number", "minimum": 0, "maximum": 1, "default": 0.2, "description": "Controls the randomness of the text output. Lower values produce more focused results. Defaults to `0.2`." }, "max_tokens": { "type": "integer", "minimum": 1, "maximum": 4096, "description": "Maximum number of tokens to generate. If omitted, uses the model default." } } }, "ContentCreateAnalyzeResponse": { "type": "object", "required": [ "text" ], "properties": { "text": { "type": "string", "description": "The generated analysis text based on the video and prompt." }, "finish_reason": { "type": "string", "enum": [ "stop", "length" ], "nullable": true, "description": "`stop` if the generation completed normally. `length` if truncated at the token limit." }, "usage": { "type": "object", "nullable": true, "properties": { "output_tokens": { "type": "integer", "description": "Number of tokens in the generated text." } } } } }, "ContentErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Unauthorized" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # List Templates Source: https://recoupable.dev/docs/api-reference/content/templates List all available content creation templates. Templates are optional — every content primitive works without one. When you do use a template, it provides a complete creative recipe: image prompts, video motion config, caption style rules, and edit operations. Returns template ID and description only — enough to pick the right one. ## GET /api/content/templates Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/content/templates": { "get": { "description": "List all available content creation templates. Templates are optional — every content primitive works without one. When you do use a template, it provides a complete creative recipe: image prompts, video motion config, caption style rules, and edit operations. Returns template ID and description only — enough to pick the right one.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Templates retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentTemplatesResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } } } } } }, "components": { "schemas": { "ContentTemplatesResponse": { "type": "object", "required": [ "templates" ], "description": "List of available content creation templates.", "properties": { "templates": { "type": "array", "items": { "$ref": "#/components/schemas/ContentTemplate" } } } }, "ContentErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Unauthorized" } } }, "ContentTemplate": { "type": "object", "required": [ "id", "description" ], "description": "A content creation template — a complete creative recipe defining visual style, composition, caption rules, and edit operations. Templates are optional; all primitives work without one.", "properties": { "id": { "type": "string", "description": "Template identifier. Pass this as the template field in content primitive requests or POST /api/content/create.", "example": "artist-caption-bedroom" }, "description": { "type": "string", "description": "Human-readable description of the template's visual style", "example": "Moody bedroom selfie. Artist on camera with deadpan expression, purple LED lighting, dark room. Short blunt captions in lowercase. Vertical 9:16 video, 8 seconds. Best for: introspective songs, vulnerable moments, daily content. Requires: face image, audio." } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Template Detail Source: https://recoupable.dev/docs/api-reference/content/template-detail Get the full configuration for a specific content creation template. Returns the complete creative recipe including image prompts, video motion config, caption style rules, and edit operations. ## GET /api/content/templates/{id} Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/content/templates/{id}": { "get": { "description": "Get the full configuration for a specific content creation template. Returns the complete creative recipe including image prompts, video motion config, caption style rules, and edit operations.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "id", "in": "path", "description": "Template identifier", "required": true, "schema": { "type": "string", "example": "artist-caption-bedroom" } } ], "responses": { "200": { "description": "Template detail retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentTemplateDetail" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "404": { "description": "Template not found", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "error" }, "error": { "type": "string", "example": "Template not found" } } } } } } } } } }, "components": { "schemas": { "ContentTemplateDetail": { "type": "object", "required": [ "id", "description" ], "description": "Full configuration for a content creation template including image prompts, video motion config, caption style rules, and edit operations.", "properties": { "id": { "type": "string", "description": "Template identifier", "example": "artist-caption-bedroom" }, "description": { "type": "string", "description": "Human-readable description of the template", "example": "Moody bedroom selfie. Artist on camera with deadpan expression, purple LED lighting, dark room." }, "image": { "type": "object", "description": "Image generation configuration", "properties": { "prompt": { "type": "string", "description": "Default image prompt used when no caller prompt is provided" }, "reference_images": { "type": "array", "items": { "type": "string", "format": "uri" }, "description": "Reference image URLs for style conditioning" }, "style_rules": { "type": "object", "description": "Style constraints (lighting, colors, composition)" } } }, "video": { "type": "object", "description": "Video generation configuration", "properties": { "moods": { "type": "array", "items": { "type": "string" }, "description": "Mood descriptors for motion prompt generation" }, "movements": { "type": "array", "items": { "type": "string" }, "description": "Camera/subject movement descriptors" } } }, "caption": { "type": "object", "description": "Caption generation configuration", "properties": { "guide": { "type": "object", "description": "Caption style guide", "properties": { "tone": { "type": "string", "description": "Voice and tone direction" }, "rules": { "type": "array", "items": { "type": "string" }, "description": "Style rules for caption generation" }, "formats": { "type": "array", "items": { "type": "string" }, "description": "Allowed caption formats" } } }, "examples": { "type": "array", "items": { "type": "string" }, "description": "Example captions for few-shot prompting" } } }, "edit": { "type": "object", "description": "Post-processing edit operations", "properties": { "operations": { "type": "array", "description": "Default edit operations applied during post-processing", "items": { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "trim", "crop", "resize", "overlay_text", "mux_audio" ], "description": "Operation type" }, "start": { "type": "number", "description": "Start time in seconds (trim)" }, "duration": { "type": "number", "description": "Duration in seconds (trim)" }, "aspect": { "type": "string", "description": "Aspect ratio e.g. 9:16 (crop)" }, "width": { "type": "integer", "description": "Width in pixels (crop/resize)" }, "height": { "type": "integer", "description": "Height in pixels (crop/resize)" }, "content": { "type": "string", "description": "Text content (overlay_text)" }, "font": { "type": "string", "description": "Font name (overlay_text)" }, "color": { "type": "string", "description": "Text color (overlay_text)" }, "stroke_color": { "type": "string", "description": "Text stroke color (overlay_text)" }, "max_font_size": { "type": "number", "description": "Maximum font size (overlay_text)" }, "position": { "type": "string", "enum": [ "top", "center", "bottom" ], "description": "Text position (overlay_text)" }, "audio_url": { "type": "string", "format": "uri", "description": "Audio URL to mux (mux_audio)" }, "replace": { "type": "boolean", "description": "Replace existing audio (mux_audio)" } } } } } } } }, "ContentErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Unauthorized" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Validate Artist Source: https://recoupable.dev/docs/api-reference/content/validate Check whether an artist has all the required files to run the content creation pipeline. Returns a structured report of each required and recommended file with its status. Required files must be present or the pipeline will fail. Recommended files improve output quality but are not strictly necessary. Provide `artist_account_id` as a query parameter. ## GET /api/content/validate Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/content/validate": { "get": { "description": "Check whether an artist has all the required files to run the content creation pipeline. Returns a structured report of each required and recommended file with its status. Required files must be present or the pipeline will fail. Recommended files improve output quality but are not strictly necessary. Provide `artist_account_id` as a query parameter.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "artist_account_id", "in": "query", "description": "UUID of the artist account to validate. Use [GET /api/artists](/docs/api-reference/artists/list) to find artist account IDs.", "required": true, "schema": { "type": "string", "format": "uuid", "example": "1873859c-dd37-4e9a-9bac-80d3558527a9" } } ], "responses": { "200": { "description": "Validation completed. Check the `ready` field to determine if the artist can run the pipeline.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentValidateResponse" } } } }, "400": { "description": "Bad request — artist_account_id is required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "404": { "description": "Artist not found — the provided artist_account_id does not match any artist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } } } } } }, "components": { "schemas": { "ContentValidateResponse": { "type": "object", "required": [ "ready", "artist_account_id" ], "description": "Validation report for an artist's content creation readiness.", "properties": { "ready": { "type": "boolean", "description": "`true` if the artist has all required files and the pipeline can run. `false` if required files are missing.", "example": true }, "artist_account_id": { "type": "string", "format": "uuid", "description": "UUID of the artist account that was validated", "example": "1873859c-dd37-4e9a-9bac-80d3558527a9" }, "songs": { "type": "integer", "description": "Number of songs found in the artist's `songs/` directory", "example": 17 }, "templates": { "type": "array", "description": "Available templates that can be used with this artist", "items": { "type": "string" }, "example": [ "artist-caption-bedroom", "artist-caption-outside", "artist-caption-stage" ] }, "checks": { "type": "object", "description": "Per-file validation results. Only present when `ready` is `true`.", "properties": { "face_guide": { "$ref": "#/components/schemas/ContentValidationCheck" }, "artist_context": { "$ref": "#/components/schemas/ContentValidationCheck" }, "audience_context": { "$ref": "#/components/schemas/ContentValidationCheck" }, "songs": { "allOf": [ { "$ref": "#/components/schemas/ContentValidationCheck" }, { "type": "object", "properties": { "count": { "type": "integer", "description": "Number of songs found", "example": 17 } } } ] } } }, "missing": { "type": "array", "description": "List of missing files with severity and fix instructions. Only present when `ready` is `false`.", "items": { "$ref": "#/components/schemas/ContentMissingFile" } } } }, "ContentErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Unauthorized" } } }, "ContentValidationCheck": { "type": "object", "required": [ "status" ], "description": "Status of a single validation check.", "properties": { "status": { "type": "string", "enum": [ "ok", "missing", "warning" ], "description": "Whether the file was found", "example": "ok" } } }, "ContentMissingFile": { "type": "object", "required": [ "file", "severity", "description", "fix" ], "description": "A missing file that prevents or degrades content creation.", "properties": { "file": { "type": "string", "description": "Relative path of the missing file within the artist directory", "example": "context/images/face-guide.png" }, "severity": { "type": "string", "enum": [ "required", "recommended" ], "description": "`required` means the pipeline will fail without this file. `recommended` means the pipeline will run but output quality is degraded.", "example": "required" }, "description": { "type": "string", "description": "What this file is used for in the pipeline", "example": "Face guide image used for AI image generation" }, "fix": { "type": "string", "description": "Actionable instructions for creating or adding the missing file", "example": "Generate a face guide using fal-ai/nano-banana-pro/edit with 2-3 reference photos of the artist" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Estimate Cost Source: https://recoupable.dev/docs/api-reference/content/estimate Estimate the cost of running the content creation pipeline. Calculates per-step and per-video costs based on current pricing. Supports comparing multiple workflow profiles (e.g., premium vs. budget) and projecting batch costs. This endpoint is informational only — it does not trigger any pipeline execution or spend credits. ## GET /api/content/estimate Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/content/estimate": { "get": { "description": "Estimate the cost of running the content creation pipeline. Calculates per-step and per-video costs based on current pricing. Supports comparing multiple workflow profiles (e.g., premium vs. budget) and projecting batch costs. This endpoint is informational only — it does not trigger any pipeline execution or spend credits.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "lipsync", "in": "query", "description": "Which workflow to estimate. `false` for image-to-video, `true` for audio-to-video. If omitted, estimates the default image-to-video workflow.", "required": false, "schema": { "type": "boolean", "example": false } }, { "name": "batch", "in": "query", "description": "Number of videos to project costs for. Use this to answer questions like \"how much would 30 videos cost?\"", "required": false, "schema": { "type": "integer", "minimum": 1, "default": 1, "example": 1 } }, { "name": "compare", "in": "query", "description": "When `true`, returns estimates for all available workflow profiles (premium, budget, mid) for side-by-side comparison.", "required": false, "schema": { "type": "boolean", "default": false, "example": false } } ], "responses": { "200": { "description": "Cost estimate calculated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentEstimateResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } } } } } }, "components": { "schemas": { "ContentEstimateResponse": { "type": "object", "required": [ "workflows" ], "description": "Cost estimates based on current pricing. When `compare` is `false`, the `workflows` array contains a single entry. When `true`, it contains all available profiles.", "properties": { "workflows": { "type": "array", "description": "One or more workflow cost breakdowns", "items": { "$ref": "#/components/schemas/ContentWorkflowEstimate" } }, "comparison": { "description": "Side-by-side summary. Only present when `compare` is `true` and multiple workflows are returned.", "$ref": "#/components/schemas/ContentEstimateComparison" }, "batch": { "type": "object", "description": "Batch cost projection. Only present when `batch` > 1.", "properties": { "count": { "type": "integer", "description": "Number of videos in the batch", "example": 30 }, "cheapestTotal": { "type": "number", "format": "float", "description": "Total cost for the batch using the cheapest workflow", "example": 5.4 }, "mostExpensiveTotal": { "type": "number", "format": "float", "description": "Total cost for the batch using the most expensive workflow", "example": 28.5 } } } } }, "ContentErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Unauthorized" } } }, "ContentWorkflowEstimate": { "type": "object", "required": [ "name", "perVideo", "steps" ], "description": "Cost breakdown for a single workflow profile.", "properties": { "name": { "type": "string", "description": "Workflow profile name", "example": "Current (image-to-video)" }, "perVideo": { "type": "number", "format": "float", "description": "Total estimated cost per video in USD", "example": 0.82 }, "steps": { "type": "array", "description": "Per-step cost breakdown showing where the money goes", "items": { "$ref": "#/components/schemas/ContentStepEstimate" } }, "costBreakdown": { "type": "object", "description": "Summary of the biggest cost driver", "properties": { "mostExpensiveStep": { "type": "string", "description": "Name of the step that costs the most", "example": "Generate Video" }, "mostExpensivePercent": { "type": "integer", "description": "Percentage of total cost from the most expensive step", "example": 68 } } } } }, "ContentEstimateComparison": { "type": "object", "required": [ "cheapest", "cheapestPerVideo", "mostExpensive", "mostExpensivePerVideo", "savingsPercent" ], "description": "Summary comparing the cheapest and most expensive workflow options.", "properties": { "cheapest": { "type": "string", "description": "Name of the cheapest workflow profile", "example": "Budget (no upscale, LTX video)" }, "cheapestPerVideo": { "type": "number", "format": "float", "description": "Cost per video for the cheapest workflow", "example": 0.18 }, "mostExpensive": { "type": "string", "description": "Name of the most expensive workflow profile", "example": "Current (audio-to-video)" }, "mostExpensivePerVideo": { "type": "number", "format": "float", "description": "Cost per video for the most expensive workflow", "example": 0.95 }, "savingsPercent": { "type": "integer", "description": "Percentage savings between the cheapest and most expensive workflows", "example": 81 } } }, "ContentStepEstimate": { "type": "object", "required": [ "name", "model", "cost", "note" ], "description": "Cost details for a single pipeline step.", "properties": { "name": { "type": "string", "description": "Pipeline step name", "example": "Generate Image" }, "model": { "type": "string", "description": "The AI model or service used for this step", "example": "fal-ai/nano-banana-pro/edit" }, "cost": { "type": "number", "format": "float", "description": "Estimated cost for this step in USD", "example": 0.04 }, "unit": { "type": "string", "description": "Billing unit type (e.g., `images`, `seconds`, `megapixels`). Not present for free steps.", "example": "images" }, "unitPrice": { "type": "number", "format": "float", "description": "Price per billing unit in USD. Not present for free steps.", "example": 0.04 }, "note": { "type": "string", "description": "Human-readable calculation detail", "example": "1 call" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Generate Image Source: https://recoupable.dev/docs/api-reference/image/generation Generate high-quality images using AI models. Images are automatically stored on Arweave and include In Process moment metadata for provenance and ownership tracking. ## GET /api/image/generate Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/image/generate": { "get": { "description": "Generate high-quality images using AI models. Images are automatically stored on Arweave and include In Process moment metadata for provenance and ownership tracking.", "parameters": [ { "name": "prompt", "in": "query", "description": "Text description of the image you want to generate", "required": true, "schema": { "type": "string" } }, { "name": "account_id", "in": "query", "description": "The unique identifier of the account generating the image", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Image generated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ImageGenerationResponse" } } } }, "400": { "description": "Bad request - missing required parameters or invalid input", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ImageGenerationErrorResponse" } } } } } } } }, "components": { "schemas": { "ImageGenerationResponse": { "type": "object", "description": "Response from the image generation endpoint, extending Experimental_GenerateImageResult from the AI SDK", "properties": { "images": { "type": "array", "items": { "$ref": "#/components/schemas/GeneratedImage" }, "description": "Array of generated image objects" }, "warnings": { "type": "array", "items": { "type": "string" }, "description": "Array of warning messages, if any" }, "responses": { "type": "array", "items": { "$ref": "#/components/schemas/ImageResponseMeta" }, "description": "Array of response metadata from the AI provider" }, "providerMetadata": { "$ref": "#/components/schemas/ImageProviderMetadata" }, "usage": { "$ref": "#/components/schemas/ImageUsage" }, "imageUrl": { "type": "string", "description": "Permanent Arweave URL where the image is stored" }, "arweaveResult": { "$ref": "#/components/schemas/ArweaveTransaction" }, "moment": { "$ref": "#/components/schemas/InProcessMoment" } } }, "ImageGenerationErrorResponse": { "type": "object", "properties": { "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "GeneratedImage": { "type": "object", "description": "A generated image file from the AI model", "properties": { "base64": { "type": "string", "description": "Image file as a base64 encoded string" }, "uint8Array": { "type": "array", "items": { "type": "integer" }, "description": "Image file as a Uint8Array (represented as array of numbers in JSON)" }, "mediaType": { "type": "string", "description": "The IANA media type of the file (e.g., 'image/png', 'image/jpeg')" } } }, "ImageResponseMeta": { "type": "object", "description": "Response metadata from the AI provider", "properties": { "finishReason": { "type": "string", "description": "Reason the generation finished (e.g., 'stop')" } } }, "ImageProviderMetadata": { "type": "object", "description": "Metadata from the AI provider about the generation", "properties": { "model": { "type": "string", "description": "The AI model used for generation (e.g., 'dall-e-3', 'gpt-image-1')" }, "size": { "type": "string", "description": "The size of the generated image (e.g., '1024x1024')" } } }, "ImageUsage": { "type": "object", "description": "Token usage information for the image generation", "properties": { "promptTokens": { "type": "integer", "description": "Number of tokens used in the prompt" }, "completionTokens": { "type": "integer", "description": "Number of tokens used in the completion (typically 0 for image generation)" }, "totalTokens": { "type": "integer", "description": "Total tokens used" } } }, "ArweaveTransaction": { "type": "object", "description": "Arweave transaction object for the stored image", "properties": { "id": { "type": "string", "description": "Unique identifier for the Arweave transaction" }, "last_tx": { "type": "string", "description": "Last transaction reference" }, "owner": { "type": "string", "description": "Owner address of the transaction" }, "tags": { "type": "array", "items": { "type": "object" }, "description": "Tags associated with the transaction" }, "target": { "type": "string", "description": "Target address (empty for data transactions)" }, "quantity": { "type": "string", "description": "Amount transferred (typically '0' for data transactions)" }, "data": { "type": "string", "description": "Transaction data (may be empty in response)" }, "reward": { "type": "string", "description": "Mining reward for the transaction" }, "signature": { "type": "string", "description": "Transaction signature" }, "format": { "type": "integer", "description": "Transaction format version" } } }, "InProcessMoment": { "type": "object", "description": "In Process moment metadata for provenance and ownership tracking", "properties": { "contractAddress": { "type": "string", "description": "Smart contract address for the moment" }, "tokenId": { "type": "string", "description": "Token ID of the minted moment" }, "hash": { "type": "string", "description": "Transaction hash of the moment mint" }, "chainId": { "type": "integer", "description": "Chain ID (e.g., 8453 for Base)" } } } } } } ``` # Transcribe Audio Source: https://recoupable.dev/docs/api-reference/transcribe/audio Transcribe audio files using OpenAI Whisper. The API saves both the original audio file and the generated markdown transcript to the customer's files in Supabase Storage. The owner account is derived from your credentials; you must have access to the specified artist_account_id. ## POST /api/transcribe Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/transcribe": { "post": { "description": "Transcribe audio files using OpenAI Whisper. The API saves both the original audio file and the generated markdown transcript to the customer's files in Supabase Storage. The owner account is derived from your credentials; you must have access to the specified artist_account_id.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Audio transcription request", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TranscribeAudioRequest" } } } }, "responses": { "200": { "description": "Audio transcribed successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TranscribeAudioResponse" } } } }, "400": { "description": "Bad request - missing required fields or invalid audio URL", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TranscribeAudioErrorResponse" } } } }, "401": { "description": "Missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TranscribeAudioErrorResponse" } } } }, "403": { "description": "Caller has no access to the specified artist_account_id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TranscribeAudioErrorResponse" } } } }, "413": { "description": "Audio file exceeds the 25MB limit", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TranscribeAudioErrorResponse" } } } }, "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TranscribeAudioErrorResponse" } } } }, "500": { "description": "Server error - OpenAI API key not configured", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TranscribeAudioErrorResponse" } } } } } } } }, "components": { "schemas": { "TranscribeAudioRequest": { "type": "object", "required": [ "audio_url", "artist_account_id" ], "properties": { "audio_url": { "type": "string", "description": "Public URL to the audio file (mp3, wav, m4a, webm)", "example": "https://example.com/song.mp3" }, "account_id": { "type": "string", "format": "uuid", "deprecated": true, "description": "Deprecated and ignored. The owner account is derived from your credentials. Sending this field is harmless but no longer affects attribution.", "example": "550e8400-e29b-41d4-a716-446655440000" }, "artist_account_id": { "type": "string", "format": "uuid", "description": "Artist account ID for file storage", "example": "550e8400-e29b-41d4-a716-446655440001" }, "title": { "type": "string", "description": "Optional title for the audio and transcription files", "example": "My Song" }, "include_timestamps": { "type": "boolean", "description": "Whether to include timestamps in the markdown transcript", "default": false } } }, "TranscribeAudioResponse": { "type": "object", "required": [ "success", "audioFile", "transcriptFile", "text" ], "properties": { "success": { "type": "boolean", "description": "Whether the transcription was successful" }, "audioFile": { "$ref": "#/components/schemas/TranscribeFileInfo", "description": "Information about the saved audio file" }, "transcriptFile": { "$ref": "#/components/schemas/TranscribeFileInfo", "description": "Information about the saved transcript file" }, "text": { "type": "string", "description": "The full transcription text" }, "language": { "type": "string", "description": "Detected language code (e.g., 'en', 'es', 'fr')" } } }, "TranscribeAudioErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "TranscribeFileInfo": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUID of the file record in the database" }, "fileName": { "type": "string", "description": "Name of the saved file" }, "storageKey": { "type": "string", "description": "Storage path in Supabase Storage" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Generate lipsync (release pending) Source: https://recoupable.dev/docs/api-reference/content/lipsync Generate audio-driven video (release pending) ## POST /api/content/lipsync Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/content/lipsync": { "post": { "summary": "Generate audio-driven video (release pending)", "description": "Available after the coordinated API and Tasks release for mono#348. Not verified on the deployed baseline. Fixed fal-ai/ltx-2-19b/audio-to-video; estimated rounded output megapixels at $0.0018 per unit, actual provider units used when supplied. The worker verifies an audio stream before final composition; presence of audio is not proof of perceptual lip alignment.", "operationId": "createContentLipsync", "tags": [ "Content" ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentCreateLipsyncRequest" } } } }, "responses": { "200": { "description": "Video generated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentCreateVideoResponse" } } } }, "400": { "description": "Validation failed — invalid or missing request body fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "402": { "description": "Insufficient credits before generation. The fixed LTX audio-to-video model costs $0.0018 per rounded output megapixel (1,800 micro-dollar credits per unit); duration and frame dimensions determine the preflight estimate.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "429": { "description": "Per-account generation rate exceeded (30 requests/minute). Retry-After states seconds to wait." }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentErrorResponse" } } } }, "502": { "description": "Provider returned no usable video; no Recoup generation charge is recorded." }, "503": { "description": "Shared generation throttling unavailable; no provider generation starts." } } } } }, "components": { "schemas": { "ContentCreateLipsyncRequest": { "type": "object", "required": [ "prompt", "image_url", "audio_url", "duration" ], "additionalProperties": false, "properties": { "prompt": { "type": "string", "minLength": 1, "maxLength": 50000 }, "image_url": { "type": "string", "format": "uri" }, "audio_url": { "type": "string", "format": "uri", "description": "Public URL of the selected song window, not the full track." }, "duration": { "type": "integer", "minimum": 5, "maximum": 19 }, "seed": { "type": "integer" } }, "description": "Release-pending fixed LTX audio-to-video primitive. 1280x720 at 25fps; bounded frame count. No arbitrary model selection.", "example": { "prompt": "The artist performs the supplied vocal phrase.", "image_url": "https://example.com/artist.png", "audio_url": "https://example.com/selected-clip.mp3", "duration": 15 } }, "ContentCreateVideoResponse": { "type": "object", "required": [ "videoUrl" ], "properties": { "videoUrl": { "type": "string", "format": "uri", "description": "URL of the generated video" } } }, "ContentErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Unauthorized" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Generate Music Source: https://recoupable.dev/docs/api-reference/music/generate Generate music ## POST /api/music Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/music": { "post": { "summary": "Generate music", "description": "Start a song generation with MiniMax Music 3. Generation runs in the background and takes roughly one to two minutes, so this returns **202** immediately with a `pending` generation. Poll [Get Music Generation](/docs/api-reference/music/get) until `status` is `completed` or `failed`; `audio_url` is populated on completion. Credits are checked before the model is called and deducted only when a generation completes, so a failed generation is never charged.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicGenerationRequest" } } } }, "responses": { "202": { "description": "Generation accepted and queued.", "headers": { "Location": { "schema": { "type": "string" }, "description": "URL of the created generation." } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicGenerationCreateResponse" } } } }, "400": { "description": "Validation failed. `missing_fields` names the first offending field.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicGenerationErrorResponse" } } } }, "401": { "description": "Unauthorized. Provide exactly one of `x-api-key` or `Authorization: Bearer`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicGenerationErrorResponse" } } } }, "402": { "description": "Insufficient credits. No generation is created and the model is not called.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicGenerationErrorResponse" } } } }, "403": { "description": "Access denied to the specified `account_id` or `organization_id`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicGenerationErrorResponse" } } } }, "500": { "description": "Internal server error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicGenerationErrorResponse" } } } } } } } }, "components": { "schemas": { "MusicGenerationRequest": { "type": "object", "required": [ "prompt", "lyrics" ], "properties": { "prompt": { "type": "string", "description": "Music description: style, mood, vocals, instrumentation and arrangement.", "example": "Genre: acoustic pop. BPM: 96. Key: C major. Warm and intimate, building gently into the chorus." }, "lyrics": { "type": "string", "description": "The lyrics to sing. Structure tags such as `[intro]`, `[verse]`, `[chorus]` and `[outro]` must each be on their own line; text on the same line as a leading tag is dropped by the model.", "example": "[verse]\nMorning light filtering through the pine\n[chorus]\nSoftly the world begins to breathe" }, "duration": { "type": "number", "minimum": 10, "maximum": 300, "default": 60, "description": "Upper bound on the generated audio length in seconds. The model may stop earlier; the actual length comes back as `duration_seconds`." }, "seed": { "type": "integer", "description": "Seed for reproducibility. Omit for a random seed." }, "num_inference_steps": { "type": "integer", "minimum": 1, "maximum": 100, "default": 30, "description": "Flow-matching steps per denoising chunk. More steps improve quality at the cost of speed." }, "guidance_scale": { "type": "number", "minimum": 0, "maximum": 20, "default": 1.7, "description": "Classifier-free guidance scale of the flow-matching stage." }, "account_id": { "type": "string", "format": "uuid", "description": "Optional. Generate on behalf of another account you can access (self, shared organization, or admin). Defaults to the calling account. Organizations are accounts, so this is also how an organization generation is scoped: send the organization's account id." } } }, "MusicGenerationCreateResponse": { "type": "object", "description": "The accepted generation. Poll it until `status` is terminal.", "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "generation": { "$ref": "#/components/schemas/MusicGeneration" } } }, "MusicGenerationErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `error`" }, "missing_fields": { "type": "array", "description": "JSON path segments to the first field that failed validation, e.g. `[\"lyrics\"]`", "items": { "oneOf": [ { "type": "string" }, { "type": "integer" } ] } }, "error": { "type": "string", "description": "Human-readable error message" } } }, "MusicGeneration": { "type": "object", "description": "One music generation.", "properties": { "id": { "type": "string", "format": "uuid", "description": "The generation's id. Poll [Get Music Generation](/docs/api-reference/music/get) with this." }, "status": { "type": "string", "enum": [ "pending", "processing", "completed", "failed" ], "description": "Lifecycle phase. `pending` until the workflow picks it up, `processing` while fal is rendering, then `completed` or `failed`." }, "prompt": { "type": "string", "description": "The music description the song was generated from." }, "lyrics": { "type": "string", "description": "The lyrics the song was generated from." }, "model": { "type": "string", "description": "The generating model.", "example": "minimax/music-3" }, "duration_seconds": { "type": [ "number", "null" ], "description": "Actual length of the generated audio. Null until completed; may be shorter than requested." }, "audio_url": { "type": [ "string", "null" ], "description": "Playable audio URL. Null until the generation completes." }, "error_message": { "type": [ "string", "null" ], "description": "Why the generation failed. Null unless `status` is `failed`." }, "created_at": { "type": "string", "format": "date-time", "description": "When the generation was created." }, "updated_at": { "type": "string", "format": "date-time", "description": "When the generation last changed." } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # List Music Generations Source: https://recoupable.dev/docs/api-reference/music/list List music generations ## GET /api/music Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/music": { "get": { "summary": "List music generations", "description": "The calling account's music generations, newest first. Scope follows the standard account override: with no `account_id` you get your own personal and organization generations; pass `account_id` to read another account you can access.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "account_id", "in": "query", "required": false, "schema": { "type": "string", "format": "uuid" }, "description": "Optional. Read another account you can access. Defaults to the calling account." }, { "name": "status", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "pending", "processing", "completed", "failed" ] }, "description": "Optional. Return only generations in this state." }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 }, "description": "Optional. Maximum generations to return, newest first." }, { "name": "offset", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 0, "default": 0 }, "description": "Optional. Number of generations to skip, for paging." } ], "responses": { "200": { "description": "Generations in the requested context, newest first.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicGenerationListResponse" } } } }, "400": { "description": "Invalid query parameter.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicGenerationErrorResponse" } } } }, "401": { "description": "Unauthorized.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicGenerationErrorResponse" } } } }, "403": { "description": "Access denied to the specified `account_id`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicGenerationErrorResponse" } } } }, "500": { "description": "Internal server error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicGenerationErrorResponse" } } } } } } } }, "components": { "schemas": { "MusicGenerationListResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "generations": { "type": "array", "description": "Generations, newest first. Empty when the context has none.", "items": { "$ref": "#/components/schemas/MusicGeneration" } } } }, "MusicGenerationErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `error`" }, "missing_fields": { "type": "array", "description": "JSON path segments to the first field that failed validation, e.g. `[\"lyrics\"]`", "items": { "oneOf": [ { "type": "string" }, { "type": "integer" } ] } }, "error": { "type": "string", "description": "Human-readable error message" } } }, "MusicGeneration": { "type": "object", "description": "One music generation.", "properties": { "id": { "type": "string", "format": "uuid", "description": "The generation's id. Poll [Get Music Generation](/docs/api-reference/music/get) with this." }, "status": { "type": "string", "enum": [ "pending", "processing", "completed", "failed" ], "description": "Lifecycle phase. `pending` until the workflow picks it up, `processing` while fal is rendering, then `completed` or `failed`." }, "prompt": { "type": "string", "description": "The music description the song was generated from." }, "lyrics": { "type": "string", "description": "The lyrics the song was generated from." }, "model": { "type": "string", "description": "The generating model.", "example": "minimax/music-3" }, "duration_seconds": { "type": [ "number", "null" ], "description": "Actual length of the generated audio. Null until completed; may be shorter than requested." }, "audio_url": { "type": [ "string", "null" ], "description": "Playable audio URL. Null until the generation completes." }, "error_message": { "type": [ "string", "null" ], "description": "Why the generation failed. Null unless `status` is `failed`." }, "created_at": { "type": "string", "format": "date-time", "description": "When the generation was created." }, "updated_at": { "type": "string", "format": "date-time", "description": "When the generation last changed." } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Music Generation Source: https://recoupable.dev/docs/api-reference/music/get Get music generation ## GET /api/music/{generationId} Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/music/{generationId}": { "get": { "summary": "Get music generation", "description": "One generation, including the `seed` the model used and its `logs` progress, both read live from the provider rather than stored. This is the polling target while a generation is in flight: call it until `status` is `completed` or `failed`. Responses are never cached.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "generationId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The generation's id, from [Generate Music](/docs/api-reference/music/generate)." }, { "name": "account_id", "in": "query", "required": false, "schema": { "type": "string", "format": "uuid" }, "description": "Optional. Read on behalf of another account you can access." } ], "responses": { "200": { "description": "The generation, with the seed it used and its render progress.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicGenerationDetailResponse" } } } }, "400": { "description": "`generationId` is not a valid uuid.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicGenerationErrorResponse" } } } }, "401": { "description": "Unauthorized.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicGenerationErrorResponse" } } } }, "403": { "description": "Access denied to this generation's account.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicGenerationErrorResponse" } } } }, "404": { "description": "No generation with that id.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicGenerationErrorResponse" } } } }, "500": { "description": "Internal server error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicGenerationErrorResponse" } } } } } } } }, "components": { "schemas": { "MusicGenerationDetailResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "generation": { "$ref": "#/components/schemas/MusicGenerationDetail" } } }, "MusicGenerationErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `error`" }, "missing_fields": { "type": "array", "description": "JSON path segments to the first field that failed validation, e.g. `[\"lyrics\"]`", "items": { "oneOf": [ { "type": "string" }, { "type": "integer" } ] } }, "error": { "type": "string", "description": "Human-readable error message" } } }, "MusicGenerationDetail": { "allOf": [ { "$ref": "#/components/schemas/MusicGeneration" }, { "type": "object", "properties": { "seed": { "type": [ "integer", "null" ], "description": "The seed the model actually used, read live from the provider rather than stored. Null until the generation completes, and null if the provider cannot be reached. Pass it back as `seed` to reproduce a generation. It is the only generation parameter recoverable after the fact: `num_inference_steps` and `guidance_scale` are consumed when the request is submitted and are never reported back." }, "logs": { "type": "array", "description": "The model's own render progress, oldest first, read live from the provider rather than stored. Empty if the provider cannot be reached. Returned only on the single-generation read.", "items": { "$ref": "#/components/schemas/MusicGenerationLogEntry" } } } } ] }, "MusicGeneration": { "type": "object", "description": "One music generation.", "properties": { "id": { "type": "string", "format": "uuid", "description": "The generation's id. Poll [Get Music Generation](/docs/api-reference/music/get) with this." }, "status": { "type": "string", "enum": [ "pending", "processing", "completed", "failed" ], "description": "Lifecycle phase. `pending` until the workflow picks it up, `processing` while fal is rendering, then `completed` or `failed`." }, "prompt": { "type": "string", "description": "The music description the song was generated from." }, "lyrics": { "type": "string", "description": "The lyrics the song was generated from." }, "model": { "type": "string", "description": "The generating model.", "example": "minimax/music-3" }, "duration_seconds": { "type": [ "number", "null" ], "description": "Actual length of the generated audio. Null until completed; may be shorter than requested." }, "audio_url": { "type": [ "string", "null" ], "description": "Playable audio URL. Null until the generation completes." }, "error_message": { "type": [ "string", "null" ], "description": "Why the generation failed. Null unless `status` is `failed`." }, "created_at": { "type": "string", "format": "date-time", "description": "When the generation was created." }, "updated_at": { "type": "string", "format": "date-time", "description": "When the generation last changed." } } }, "MusicGenerationLogEntry": { "type": "object", "description": "One line of the model's render progress.", "properties": { "at": { "type": "string", "format": "date-time", "description": "When the line was emitted." }, "message": { "type": "string", "description": "What the model reported.", "example": "20/180" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Artist Posts Source: https://recoupable.dev/docs/api-reference/posts/get Retrieve all social media posts from an artist across all platforms. This endpoint aggregates posts from all connected social media profiles for the specified artist, newest first. Engagement counts (`views`, `likes`, `comments`, `reposts`) are the values captured by the most recent scrape of that profile; they are null on platforms that do not report them and on posts stored before engagement capture. Supports pagination for large post collections. ## GET /api/artists/{id}/posts Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/artists/{id}/posts": { "get": { "description": "Retrieve all social media posts from an artist across all platforms. This endpoint aggregates posts from all connected social media profiles for the specified artist, newest first. Engagement counts (`views`, `likes`, `comments`, `reposts`) are the values captured by the most recent scrape of that profile; they are null on platforms that do not report them and on posts stored before engagement capture. Supports pagination for large post collections.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier of the artist account to fetch posts for", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "page", "in": "query", "description": "The page number to retrieve (default: 1)", "required": false, "schema": { "type": "integer", "default": 1 } }, { "name": "limit", "in": "query", "description": "The number of records per page (default: 20, max: 100)", "required": false, "schema": { "type": "integer", "default": 20, "maximum": 100 } } ], "responses": { "200": { "description": "Artist posts retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistPostsResponse" } } } }, "400": { "description": "Bad request - invalid or missing `id` path parameter format", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistPostsErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistPostsErrorResponse" } } } }, "403": { "description": "Forbidden - caller does not have access to this artist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistPostsErrorResponse" } } } }, "404": { "description": "Artist not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistPostsErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistPostsErrorResponse" } } } } } } } }, "components": { "schemas": { "ArtistPostsResponse": { "type": "object", "required": [ "status", "posts", "pagination" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "posts": { "type": "array", "items": { "$ref": "#/components/schemas/ArtistPost" }, "description": "List of posts from the artist across all social platforms" }, "pagination": { "$ref": "#/components/schemas/ArtistPostsPagination", "description": "Pagination metadata for the response" } } }, "ArtistPostsErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "ArtistPost": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the post" }, "post_url": { "type": "string", "description": "Direct URL to the post on the social platform" }, "updated_at": { "type": "string", "format": "date-time", "description": "ISO timestamp of when the post was published on the platform" }, "views": { "type": "integer", "nullable": true, "description": "View or play count at the last scrape. YouTube `viewCount`, TikTok `playCount`, X `viewCount`. Null on Instagram, LinkedIn, Threads and Facebook." }, "likes": { "type": "integer", "nullable": true, "description": "Like count at the last scrape. Instagram `likesCount`, TikTok `diggCount`, YouTube `likes`, X `likeCount`, LinkedIn `likes`. Null on Threads and Facebook." }, "comments": { "type": "integer", "nullable": true, "description": "Comment count at the last scrape. Instagram `commentsCount`, TikTok `commentCount`, YouTube `commentsCount`, X `replyCount`, LinkedIn `comments`. Null on Threads and Facebook." }, "reposts": { "type": "integer", "nullable": true, "description": "Share, repost or retweet count at the last scrape. TikTok `shareCount`, X `retweetCount`, LinkedIn `shares`. Null on Instagram, YouTube, Threads and Facebook." } } }, "ArtistPostsPagination": { "type": "object", "properties": { "total_count": { "type": "integer", "description": "Total number of posts available" }, "page": { "type": "integer", "description": "Current page number" }, "limit": { "type": "integer", "description": "Number of posts per page" }, "total_pages": { "type": "integer", "description": "Total number of pages available" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Comments Source: https://recoupable.dev/docs/api-reference/comments/get **Availability: pending reconciliation.** The legacy comments read route has no matching route in the inspected API source. The comments and fan-recency workflow remains a retained product outcome pending its public read integration. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation. ## GET /api/comments Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/comments": { "get": { "description": "**Availability: pending reconciliation.** The legacy comments read route has no matching route in the inspected API source. The comments and fan-recency workflow remains a retained product outcome pending its public read integration. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation.\n\nRetrieve comments associated with an artist or a specific post, with support for pagination. This endpoint returns raw comment data including the comment text, associated post, and commenter's social profile reference.", "parameters": [ { "name": "artist_account_id", "in": "query", "description": "The unique identifier of the artist account to fetch comments for", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "post_id", "in": "query", "description": "Filter comments by specific post", "required": false, "schema": { "type": "string", "format": "uuid" } }, { "name": "page", "in": "query", "description": "Page number for pagination (default: 1)", "required": false, "schema": { "type": "integer", "default": 1 } }, { "name": "limit", "in": "query", "description": "Number of comments per page (default: 10)", "required": false, "schema": { "type": "integer", "default": 10 } } ], "responses": { "200": { "description": "Comments retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CommentsResponse" } } } }, "400": { "description": "Bad request - missing required parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CommentsErrorResponse" } } } } }, "x-recoup-availability": "pending" } } }, "components": { "schemas": { "CommentsResponse": { "type": "object", "required": [ "status", "comments", "pagination" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "comments": { "type": "array", "items": { "$ref": "#/components/schemas/Comment" }, "description": "List of comments for the specified artist or post" }, "pagination": { "$ref": "#/components/schemas/CommentsPagination", "description": "Pagination metadata for the response" } } }, "CommentsErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "Comment": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUID of the comment" }, "post_id": { "type": "string", "format": "uuid", "description": "UUID of the associated post" }, "social_id": { "type": "string", "format": "uuid", "description": "UUID of the social profile who made the comment" }, "comment": { "type": "string", "description": "Comment text content" }, "commented_at": { "type": "string", "format": "date-time", "description": "Timestamp with timezone of when the comment was made" } } }, "CommentsPagination": { "type": "object", "properties": { "total_count": { "type": "integer", "description": "Total number of comments available" }, "page": { "type": "integer", "description": "Current page number" }, "limit": { "type": "integer", "description": "Number of comments per page" }, "total_pages": { "type": "integer", "description": "Total number of pages available" } } } } } } ``` # Social Scrape Source: https://recoupable.dev/docs/api-reference/social/scrape Trigger a scrape of one social profile. Use [Get Artist Socials](/docs/api-reference/artists/socials) first to get the social `id`. The response contains scraper run metadata; poll it with the [Scraper Results API](/docs/api-reference/apify/scraper). ## POST /api/socials/{id}/scrape Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/socials/{id}/scrape": { "post": { "description": "Trigger a scrape of one social profile. Use [Get Artist Socials](/docs/api-reference/artists/socials) first to get the social `id`. The response contains scraper run metadata; poll it with the [Scraper Results API](/docs/api-reference/apify/scraper).\n\n**Credits:** `50,000 + 10,000 × posts` micro-dollar credits per profile: $0.05 base + $0.01 per requested post. Omitting `posts` charges the 50,000 base, even when the selected platform fetches a default depth.\n\n**What `posts` does per platform**\n\n| Platform | `posts` | When `posts` is omitted |\n|---|---|---|\n| Instagram | ignored | the profile's latest posts (up to 12), always |\n| TikTok | `resultsPerPage` | 1 video |\n| YouTube | `maxResults`, `maxResultsShorts`, `maxResultStreams` (per content type) | 1 video, 0 Shorts, 0 streams |\n| X / Twitter | timeline items; retweets and replies are fetched but not stored | 10 timeline items before filtering |\n| LinkedIn | `maxPosts` on the posts actor (runs instead of the profile actor) | profile only, no posts |\n| Threads | ignored | profile only |\n| Facebook | ignored | profile only |\n\n**What gets stored.** Webhook processing persists supported records from successful runs. Platform filtering, missing fields and budget-limited follow-ups can leave partial results:\n\n- The profile: username, avatar, bio, follower and following counts, region. Served by [Get Artist Socials](/docs/api-reference/artists/socials).\n- One follower snapshot per social per day whenever the run reports a follower count (following and post counts too, where the platform reports them). Served by the `history` parameter of Get Artist Socials.\n- Every returned post with its engagement (`views`, `likes`, `comments`, `reposts`, where the platform reports them). Served by [Get Artist Posts](/docs/api-reference/posts/get).\n- Instagram only: comments batches for already-known and newly discovered posts, then terminal profile batches for the returned commenters. Available profile fields are saved; completion and field coverage are not guaranteed. Served by [Get Artist Fans](/docs/api-reference/fans/get).\n\n**Fan discovery is one hop deep.** The commenter profile run is terminal: it never schedules comments or profile runs of its own. A normal single delivery can produce one profile run, two comments batches and two terminal commenter-profile batches. Five is a derived case, not a universal bound: retries, concurrent deliveries and budget checks affect the outcome. Budget checks are not atomic reservations and may allow overshoot; bookkeeping failures do not guarantee a hard cap. Pass `subtitles=true` on a YouTube social to include each video's transcript in the run results.", "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier (UUID) of the social profile to scrape. Obtain this from the [Get Artist Socials](/docs/api-reference/artists/socials) API.", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "posts", "in": "query", "description": "Recent-post depth. Honored on TikTok, YouTube (per content type), X and LinkedIn; ignored on Instagram (always the profile's latest posts, up to 12), Threads and Facebook. Without it TikTok and YouTube use their single-item defaults; X fetches ten timeline items before filtering, so pass it whenever you need post history. Every returned post is stored with its engagement counts. See the per-platform table above.", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100 }, "example": 10 }, { "name": "subtitles", "in": "query", "required": false, "description": "YouTube only. When `true`, the scrape also downloads each returned video's captions (auto-generated captions preferred, English, plain text). The transcript is returned on the run's dataset items as `subtitles[0].plaintext` and is not persisted; read it from [Scraper Results](/docs/api-reference/apify/scraper) while the run's dataset is available. Ignored on every other platform. Adds no credits beyond the `posts` price.", "schema": { "type": "boolean", "default": false } } ], "responses": { "200": { "description": "Scrape job triggered successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApifyRunResult" } } } }, "400": { "description": "Invalid path/query, malformed input or unsupported platform.", "content": { "application/json": { "schema": { "anyOf": [ { "$ref": "#/components/schemas/SocialScrapeErrorResponse" }, { "type": "object", "required": [ "runId", "datasetId", "error" ], "properties": { "runId": { "type": "null" }, "datasetId": { "type": "null" }, "error": { "type": "string", "enum": [ "Unsupported social media platform" ] } } } ] } } } }, "401": { "description": "Missing, invalid or conflicting authentication headers.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SocialScrapeErrorResponse" } } } }, "402": { "description": "Insufficient credits — the body includes a static `billingUrl`. No Stripe object is created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SocialInsufficientCreditsResponse" } } } }, "403": { "description": "The caller cannot access an artist linked to this social profile.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SocialScrapeErrorResponse" } } } }, "404": { "description": "The social profile was not found.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SocialScrapeErrorResponse" } } } }, "500": { "description": "Scraper startup, persistence or an internal operation failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SocialScrapeErrorResponse" } } } } }, "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ] } } }, "components": { "schemas": { "ApifyRunResult": { "type": "object", "properties": { "runId": { "type": "string", "description": "Unique identifier for the Apify run" }, "datasetId": { "type": "string", "description": "Unique identifier for the dataset containing scraped data" }, "error": { "type": "string", "nullable": true, "description": "Error message if the run failed (null if successful)" } } }, "SocialScrapeErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ] }, "error": { "type": "string" }, "missing_fields": { "type": "array", "items": { "type": [ "string", "integer" ] } } } }, "SocialInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks credits for the scrape. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create). Example: one profile with posts=20 requires 250,000 micro-dollar credits ($0.25).", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 250000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Scrape Artist Socials Source: https://recoupable.dev/docs/api-reference/artist/socials-scrape Trigger scrape jobs for all social profiles linked to an artist. Returns a runId per social profile that you can poll for status and results via the [Scraper Results API](/docs/api-reference/apify/scraper). ## POST /api/artist/socials/scrape Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/artist/socials/scrape": { "post": { "description": "Trigger scrape jobs for all social profiles linked to an artist. Returns a runId per social profile that you can poll for status and results via the [Scraper Results API](/docs/api-reference/apify/scraper).\n\n**Credits:** `50,000 + 10,000 × posts` micro-dollar credits per profile: $0.05 base + $0.01 per requested post. Omitting posts uses the 50,000 base. The upfront balance check covers every linked profile; deduction covers only profiles with a started scraper run ID. Per-profile start errors remain in the response and are not charged when no run starts. For two linked profiles and posts=20, the balance gate requires 500,000 credits ($0.50); one started run charges 250,000 credits ($0.25).", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Artist to scrape socials for", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistSocialsScrapeRequest" } } } }, "responses": { "200": { "description": "Scrape jobs triggered successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistSocialsScrapeResponse" } } } }, "400": { "description": "Bad request - missing required parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistSocialsErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistSocialsErrorResponse" } } } }, "402": { "description": "Insufficient credits — the body includes a static `billingUrl`. No Stripe object is created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistSocialsInsufficientCreditsResponse" } } } }, "403": { "description": "Forbidden - caller does not have access to this artist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistSocialsErrorResponse" } } } } } } } }, "components": { "schemas": { "ArtistSocialsScrapeRequest": { "type": "object", "required": [ "artist_account_id" ], "properties": { "artist_account_id": { "type": "string", "format": "uuid", "description": "UUID of the artist account to scrape socials for", "example": "1873859c-dd37-4e9a-9bac-80d35a1b2c3d" }, "posts": { "type": "integer", "minimum": 1, "maximum": 100, "example": 10, "description": "Recent-post depth applied to every profile in the batch. Honored on TikTok, YouTube (per content type), X and LinkedIn; ignored on Instagram (always the profile's latest posts, up to 12), Threads and Facebook. Without it TikTok, YouTube and X return a single latest item. Every returned post is stored with its engagement counts; see the per-platform table on [Social Scrape](/docs/api-reference/social/scrape). Credits: `5 + posts` per profile." } } }, "ArtistSocialsScrapeResponse": { "type": "array", "items": { "$ref": "#/components/schemas/ApifyRunResult" }, "description": "Array of Apify run results, one for each social profile scraped" }, "ArtistSocialsErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } }, "ArtistSocialsInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks credits for the scrapes. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create). Example: two linked profiles with posts=20 require 500,000 micro-dollar credits ($0.50) at the upfront balance gate.", "properties": { "error": { "type": "string", "enum": [ "insufficient_credits" ] }, "remaining_credits": { "type": "integer", "example": 0 }, "required_credits": { "type": "integer", "example": 500000 }, "billingUrl": { "type": "string", "description": "Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call [`POST /api/credits/sessions`](/docs/api-reference/credits/sessions-create).", "example": "https://app.recoupable.dev" } }, "required": [ "error", "remaining_credits", "required_credits", "billingUrl" ] }, "ApifyRunResult": { "type": "object", "properties": { "runId": { "type": "string", "description": "Unique identifier for the Apify run" }, "datasetId": { "type": "string", "description": "Unique identifier for the dataset containing scraped data" }, "error": { "type": "string", "nullable": true, "description": "Error message if the run failed (null if successful)" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Search Source: https://recoupable.dev/docs/api-reference/spotify/search Search for artists, albums, tracks, and playlists using the Spotify API. This endpoint is a proxy to the official Spotify Search API. ## GET /api/spotify/search Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/spotify/search": { "get": { "description": "Search for artists, albums, tracks, and playlists using the Spotify API. This endpoint is a proxy to the official Spotify Search API.", "parameters": [ { "name": "q", "in": "query", "description": "The search query keywords and optional field filters", "required": true, "schema": { "type": "string" } }, { "name": "type", "in": "query", "description": "A comma-separated list of item types to search across: album, artist, playlist, track, show, episode, audiobook", "required": true, "schema": { "type": "string" } }, { "name": "market", "in": "query", "description": "An ISO 3166-1 alpha-2 country code or 'from_token'", "required": false, "schema": { "type": "string" } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (default: 20, min: 1, max: 50)", "required": false, "schema": { "type": "integer", "default": 20, "minimum": 1, "maximum": 50 } }, { "name": "offset", "in": "query", "description": "The index of the first result to return (default: 0, max: 10000)", "required": false, "schema": { "type": "integer", "default": 0, "maximum": 10000 } } ], "responses": { "200": { "description": "Search results retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpotifySearchResponse" } } } }, "400": { "description": "Bad request - missing required parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpotifyErrorResponse" } } } } } } } }, "components": { "schemas": { "SpotifySearchResponse": { "type": "object", "properties": { "artists": { "$ref": "#/components/schemas/SpotifyArtistsPaginated", "description": "Search results for artists (if type includes artist)" }, "albums": { "type": "object", "description": "Search results for albums (if type includes album)" }, "tracks": { "type": "object", "description": "Search results for tracks (if type includes track)" }, "playlists": { "type": "object", "description": "Search results for playlists (if type includes playlist)" } } }, "SpotifyErrorResponse": { "type": "object", "properties": { "error": { "type": "object", "properties": { "status": { "type": "integer", "description": "HTTP status code" }, "message": { "type": "string", "description": "Error message" } }, "description": "Error details" } } }, "SpotifyArtistsPaginated": { "type": "object", "properties": { "href": { "type": "string", "description": "A link to the Web API endpoint returning the full result" }, "items": { "type": "array", "items": { "$ref": "#/components/schemas/SpotifyArtistObject" }, "description": "List of artist objects" }, "limit": { "type": "integer", "description": "The maximum number of items in the response" }, "next": { "type": "string", "nullable": true, "description": "URL to the next page of items" }, "offset": { "type": "integer", "description": "The offset of the items returned" }, "previous": { "type": "string", "nullable": true, "description": "URL to the previous page of items" }, "total": { "type": "integer", "description": "The total number of items available" } } }, "SpotifyArtistObject": { "type": "object", "properties": { "external_urls": { "$ref": "#/components/schemas/SpotifyExternalUrls" }, "followers": { "$ref": "#/components/schemas/SpotifyFollowers" }, "genres": { "type": "array", "items": { "type": "string" }, "description": "A list of the genres the artist is associated with" }, "href": { "type": "string", "description": "A link to the Web API endpoint providing full details" }, "id": { "type": "string", "description": "The Spotify ID for the artist" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/SpotifyImage" }, "description": "Images of the artist in various sizes" }, "name": { "type": "string", "description": "The name of the artist" }, "popularity": { "type": "integer", "description": "The popularity of the artist (0-100)" }, "type": { "type": "string", "enum": [ "artist" ], "description": "The object type, always 'artist'" }, "uri": { "type": "string", "description": "The Spotify URI for the artist" } } }, "SpotifyExternalUrls": { "type": "object", "properties": { "spotify": { "type": "string", "description": "The Spotify URL for the object" } } }, "SpotifyFollowers": { "type": "object", "properties": { "href": { "type": "string", "nullable": true, "description": "This will always be set to null" }, "total": { "type": "integer", "description": "The total number of followers" } } }, "SpotifyImage": { "type": "object", "properties": { "url": { "type": "string", "description": "The source URL of the image" }, "height": { "type": "integer", "nullable": true, "description": "The image height in pixels" }, "width": { "type": "integer", "nullable": true, "description": "The image width in pixels" } } } } } } ``` # Get Artist Source: https://recoupable.dev/docs/api-reference/spotify/artist Get Spotify catalog information for a single artist identified by their unique Spotify ID. ## GET /api/spotify/artist/ Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/spotify/artist/": { "get": { "description": "Get Spotify catalog information for a single artist identified by their unique Spotify ID.", "parameters": [ { "name": "id", "in": "query", "description": "The Spotify ID of the artist", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Artist retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpotifyGetArtistResponse" } } } }, "400": { "description": "Bad request - missing required parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpotifyErrorResponse" } } } } } } } }, "components": { "schemas": { "SpotifyGetArtistResponse": { "type": "object", "properties": { "artist": { "$ref": "#/components/schemas/SpotifyArtistObject", "nullable": true, "description": "The Spotify artist object (null if error)" }, "error": { "type": "object", "nullable": true, "description": "Error object if request failed (null if successful)" } } }, "SpotifyErrorResponse": { "type": "object", "properties": { "error": { "type": "object", "properties": { "status": { "type": "integer", "description": "HTTP status code" }, "message": { "type": "string", "description": "Error message" } }, "description": "Error details" } } }, "SpotifyArtistObject": { "type": "object", "properties": { "external_urls": { "$ref": "#/components/schemas/SpotifyExternalUrls" }, "followers": { "$ref": "#/components/schemas/SpotifyFollowers" }, "genres": { "type": "array", "items": { "type": "string" }, "description": "A list of the genres the artist is associated with" }, "href": { "type": "string", "description": "A link to the Web API endpoint providing full details" }, "id": { "type": "string", "description": "The Spotify ID for the artist" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/SpotifyImage" }, "description": "Images of the artist in various sizes" }, "name": { "type": "string", "description": "The name of the artist" }, "popularity": { "type": "integer", "description": "The popularity of the artist (0-100)" }, "type": { "type": "string", "enum": [ "artist" ], "description": "The object type, always 'artist'" }, "uri": { "type": "string", "description": "The Spotify URI for the artist" } } }, "SpotifyExternalUrls": { "type": "object", "properties": { "spotify": { "type": "string", "description": "The Spotify URL for the object" } } }, "SpotifyFollowers": { "type": "object", "properties": { "href": { "type": "string", "nullable": true, "description": "This will always be set to null" }, "total": { "type": "integer", "description": "The total number of followers" } } }, "SpotifyImage": { "type": "object", "properties": { "url": { "type": "string", "description": "The source URL of the image" }, "height": { "type": "integer", "nullable": true, "description": "The image height in pixels" }, "width": { "type": "integer", "nullable": true, "description": "The image width in pixels" } } } } } } ``` # Get Artist Albums Source: https://recoupable.dev/docs/api-reference/spotify/artist-albums Get Spotify catalog information about an artist's albums. ## GET /api/spotify/artist/albums Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/spotify/artist/albums": { "get": { "description": "Get Spotify catalog information about an artist's albums.", "parameters": [ { "name": "id", "in": "query", "description": "The Spotify ID of the artist", "required": true, "schema": { "type": "string" } }, { "name": "include_groups", "in": "query", "description": "A comma-separated list of keywords to filter the response. Valid values are: album, single, appears_on, compilation", "required": false, "schema": { "type": "string" } }, { "name": "market", "in": "query", "description": "An ISO 3166-1 alpha-2 country code. If specified, only content available in that market will be returned", "required": false, "schema": { "type": "string" } }, { "name": "limit", "in": "query", "description": "The maximum number of items to return (default: 20, min: 1, max: 50)", "required": false, "schema": { "type": "integer", "default": 20, "minimum": 1, "maximum": 50 } }, { "name": "offset", "in": "query", "description": "The index of the first item to return (default: 0)", "required": false, "schema": { "type": "integer", "default": 0 } } ], "responses": { "200": { "description": "Artist albums retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpotifyArtistAlbumsResponse" } } } }, "400": { "description": "Bad request - missing required parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpotifyErrorResponse" } } } } } } } }, "components": { "schemas": { "SpotifyArtistAlbumsResponse": { "type": "object", "properties": { "href": { "type": "string", "description": "A link to the Web API endpoint returning the full result" }, "limit": { "type": "integer", "description": "The maximum number of items in the response" }, "next": { "type": "string", "nullable": true, "description": "URL to the next page of items" }, "offset": { "type": "integer", "description": "The offset of the items returned" }, "previous": { "type": "string", "nullable": true, "description": "URL to the previous page of items" }, "total": { "type": "integer", "description": "The total number of items available" }, "items": { "type": "array", "items": { "$ref": "#/components/schemas/SpotifySimplifiedAlbum" }, "description": "Array of simplified album objects" } } }, "SpotifyErrorResponse": { "type": "object", "properties": { "error": { "type": "object", "properties": { "status": { "type": "integer", "description": "HTTP status code" }, "message": { "type": "string", "description": "Error message" } }, "description": "Error details" } } }, "SpotifySimplifiedAlbum": { "type": "object", "properties": { "album_type": { "type": "string", "enum": [ "album", "single", "compilation" ], "description": "The type of the album" }, "total_tracks": { "type": "integer", "description": "The number of tracks in the album" }, "available_markets": { "type": "array", "items": { "type": "string" }, "description": "Markets in which the album is available" }, "external_urls": { "$ref": "#/components/schemas/SpotifyExternalUrls" }, "href": { "type": "string", "description": "A link to the Web API endpoint providing full details" }, "id": { "type": "string", "description": "The Spotify ID for the album" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/SpotifyImage" }, "description": "The cover art for the album in various sizes" }, "name": { "type": "string", "description": "The name of the album" }, "release_date": { "type": "string", "description": "The date the album was first released" }, "release_date_precision": { "type": "string", "enum": [ "year", "month", "day" ], "description": "The precision with which release_date value is known" }, "restrictions": { "type": "object", "properties": { "reason": { "type": "string", "description": "The reason for the restriction" } }, "description": "Included when a content restriction is applied" }, "type": { "type": "string", "enum": [ "album" ], "description": "The object type, always 'album'" }, "uri": { "type": "string", "description": "The Spotify URI for the album" }, "artists": { "type": "array", "items": { "$ref": "#/components/schemas/SpotifySimplifiedArtist" }, "description": "The artists of the album" }, "album_group": { "type": "string", "enum": [ "album", "single", "compilation", "appears_on" ], "description": "The field to distinguish albums by various groups" } } }, "SpotifyExternalUrls": { "type": "object", "properties": { "spotify": { "type": "string", "description": "The Spotify URL for the object" } } }, "SpotifyImage": { "type": "object", "properties": { "url": { "type": "string", "description": "The source URL of the image" }, "height": { "type": "integer", "nullable": true, "description": "The image height in pixels" }, "width": { "type": "integer", "nullable": true, "description": "The image width in pixels" } } }, "SpotifySimplifiedArtist": { "type": "object", "properties": { "external_urls": { "$ref": "#/components/schemas/SpotifyExternalUrls" }, "href": { "type": "string", "description": "A link to the Web API endpoint providing full details" }, "id": { "type": "string", "description": "The Spotify ID for the artist" }, "name": { "type": "string", "description": "The name of the artist" }, "type": { "type": "string", "enum": [ "artist" ], "description": "The object type, always 'artist'" }, "uri": { "type": "string", "description": "The Spotify URI for the artist" } } } } } } ``` # Get Artist Top Tracks Source: https://recoupable.dev/docs/api-reference/spotify/artist-top-tracks Get an artist's top tracks by country. ## GET /api/spotify/artist/topTracks Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/spotify/artist/topTracks": { "get": { "description": "Get an artist's top tracks by country.", "parameters": [ { "name": "id", "in": "query", "description": "The Spotify ID of the artist", "required": true, "schema": { "type": "string" } }, { "name": "market", "in": "query", "description": "An ISO 3166-1 alpha-2 country code. If provided, only tracks available in that market are returned", "required": false, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Artist top tracks retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpotifyArtistTopTracksResponse" } } } }, "400": { "description": "Bad request - missing required parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpotifyErrorResponse" } } } } } } } }, "components": { "schemas": { "SpotifyArtistTopTracksResponse": { "type": "object", "properties": { "tracks": { "type": "array", "items": { "$ref": "#/components/schemas/SpotifyTrack" }, "description": "Array of track objects" } } }, "SpotifyErrorResponse": { "type": "object", "properties": { "error": { "type": "object", "properties": { "status": { "type": "integer", "description": "HTTP status code" }, "message": { "type": "string", "description": "Error message" } }, "description": "Error details" } } }, "SpotifyTrack": { "type": "object", "properties": { "album": { "$ref": "#/components/schemas/SpotifySimplifiedAlbum", "description": "The album the track appears on" }, "artists": { "type": "array", "items": { "$ref": "#/components/schemas/SpotifySimplifiedArtist" }, "description": "Artists who performed the track" }, "available_markets": { "type": "array", "items": { "type": "string" }, "description": "Markets in which the track is available" }, "disc_number": { "type": "integer", "description": "Disc number the track is on" }, "duration_ms": { "type": "integer", "description": "Track length in milliseconds" }, "explicit": { "type": "boolean", "description": "Whether the track has explicit lyrics" }, "external_ids": { "type": "object", "properties": { "isrc": { "type": "string", "description": "International Standard Recording Code" }, "ean": { "type": "string", "description": "International Article Number" }, "upc": { "type": "string", "description": "Universal Product Code" } }, "description": "Known external IDs for the track" }, "external_urls": { "$ref": "#/components/schemas/SpotifyExternalUrls" }, "href": { "type": "string", "description": "Link to the Web API endpoint with full details" }, "id": { "type": "string", "description": "Spotify ID for the track" }, "is_playable": { "type": "boolean", "description": "If true, the track is playable in the given market" }, "linked_from": { "type": "object", "description": "Information about the originally requested track when track relinking is applied" }, "restrictions": { "type": "object", "properties": { "reason": { "type": "string", "description": "The reason for the restriction" } }, "description": "Content restriction information" }, "name": { "type": "string", "description": "Track name" }, "popularity": { "type": "integer", "description": "Popularity score (0-100)" }, "preview_url": { "type": "string", "nullable": true, "description": "URL to a 30 second preview, if available" }, "track_number": { "type": "integer", "description": "Track number on the album" }, "type": { "type": "string", "enum": [ "track" ], "description": "The object type, always 'track'" }, "uri": { "type": "string", "description": "The Spotify URI for the track" }, "is_local": { "type": "boolean", "description": "Whether the track is from a local file" } } }, "SpotifySimplifiedAlbum": { "type": "object", "properties": { "album_type": { "type": "string", "enum": [ "album", "single", "compilation" ], "description": "The type of the album" }, "total_tracks": { "type": "integer", "description": "The number of tracks in the album" }, "available_markets": { "type": "array", "items": { "type": "string" }, "description": "Markets in which the album is available" }, "external_urls": { "$ref": "#/components/schemas/SpotifyExternalUrls" }, "href": { "type": "string", "description": "A link to the Web API endpoint providing full details" }, "id": { "type": "string", "description": "The Spotify ID for the album" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/SpotifyImage" }, "description": "The cover art for the album in various sizes" }, "name": { "type": "string", "description": "The name of the album" }, "release_date": { "type": "string", "description": "The date the album was first released" }, "release_date_precision": { "type": "string", "enum": [ "year", "month", "day" ], "description": "The precision with which release_date value is known" }, "restrictions": { "type": "object", "properties": { "reason": { "type": "string", "description": "The reason for the restriction" } }, "description": "Included when a content restriction is applied" }, "type": { "type": "string", "enum": [ "album" ], "description": "The object type, always 'album'" }, "uri": { "type": "string", "description": "The Spotify URI for the album" }, "artists": { "type": "array", "items": { "$ref": "#/components/schemas/SpotifySimplifiedArtist" }, "description": "The artists of the album" }, "album_group": { "type": "string", "enum": [ "album", "single", "compilation", "appears_on" ], "description": "The field to distinguish albums by various groups" } } }, "SpotifySimplifiedArtist": { "type": "object", "properties": { "external_urls": { "$ref": "#/components/schemas/SpotifyExternalUrls" }, "href": { "type": "string", "description": "A link to the Web API endpoint providing full details" }, "id": { "type": "string", "description": "The Spotify ID for the artist" }, "name": { "type": "string", "description": "The name of the artist" }, "type": { "type": "string", "enum": [ "artist" ], "description": "The object type, always 'artist'" }, "uri": { "type": "string", "description": "The Spotify URI for the artist" } } }, "SpotifyExternalUrls": { "type": "object", "properties": { "spotify": { "type": "string", "description": "The Spotify URL for the object" } } }, "SpotifyImage": { "type": "object", "properties": { "url": { "type": "string", "description": "The source URL of the image" }, "height": { "type": "integer", "nullable": true, "description": "The image height in pixels" }, "width": { "type": "integer", "nullable": true, "description": "The image width in pixels" } } } } } } ``` # Get Album Source: https://recoupable.dev/docs/api-reference/spotify/album Get Spotify catalog information for a single album. ## GET /api/spotify/album Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/spotify/album": { "get": { "description": "Get Spotify catalog information for a single album.", "parameters": [ { "name": "id", "in": "query", "description": "The Spotify ID of the album", "required": true, "schema": { "type": "string" } }, { "name": "market", "in": "query", "description": "An ISO 3166-1 alpha-2 country code. If provided, only content available in that market is returned", "required": false, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Album retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpotifyAlbum" } } } }, "400": { "description": "Bad request - missing required parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpotifyErrorResponse" } } } } } } } }, "components": { "schemas": { "SpotifyAlbum": { "type": "object", "properties": { "album_type": { "type": "string", "enum": [ "album", "single", "compilation" ], "description": "The type of the album" }, "total_tracks": { "type": "integer", "description": "The number of tracks in the album" }, "available_markets": { "type": "array", "items": { "type": "string" }, "description": "Markets in which the album is available" }, "external_urls": { "$ref": "#/components/schemas/SpotifyExternalUrls" }, "href": { "type": "string", "description": "A link to the Web API endpoint providing full details" }, "id": { "type": "string", "description": "The Spotify ID for the album" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/SpotifyImage" }, "description": "The cover art for the album in various sizes" }, "name": { "type": "string", "description": "The name of the album" }, "release_date": { "type": "string", "description": "The date the album was first released" }, "release_date_precision": { "type": "string", "enum": [ "year", "month", "day" ], "description": "The precision with which release_date value is known" }, "restrictions": { "type": "object", "properties": { "reason": { "type": "string" } }, "description": "Included when a content restriction is applied" }, "type": { "type": "string", "enum": [ "album" ], "description": "The object type, always 'album'" }, "uri": { "type": "string", "description": "The Spotify URI for the album" }, "artists": { "type": "array", "items": { "$ref": "#/components/schemas/SpotifySimplifiedArtist" }, "description": "The artists of the album" }, "tracks": { "$ref": "#/components/schemas/SpotifyAlbumTracks", "description": "The tracks of the album" }, "copyrights": { "type": "array", "items": { "$ref": "#/components/schemas/SpotifyCopyright" }, "description": "Copyright statements of the album" }, "external_ids": { "type": "object", "properties": { "isrc": { "type": "string" }, "ean": { "type": "string" }, "upc": { "type": "string" } }, "description": "Known external IDs for the album" }, "genres": { "type": "array", "items": { "type": "string" }, "description": "Deprecated. Always empty." }, "label": { "type": "string", "description": "The label associated with the album" }, "popularity": { "type": "integer", "description": "Popularity of the album (0-100)" } } }, "SpotifyErrorResponse": { "type": "object", "properties": { "error": { "type": "object", "properties": { "status": { "type": "integer", "description": "HTTP status code" }, "message": { "type": "string", "description": "Error message" } }, "description": "Error details" } } }, "SpotifyExternalUrls": { "type": "object", "properties": { "spotify": { "type": "string", "description": "The Spotify URL for the object" } } }, "SpotifyImage": { "type": "object", "properties": { "url": { "type": "string", "description": "The source URL of the image" }, "height": { "type": "integer", "nullable": true, "description": "The image height in pixels" }, "width": { "type": "integer", "nullable": true, "description": "The image width in pixels" } } }, "SpotifySimplifiedArtist": { "type": "object", "properties": { "external_urls": { "$ref": "#/components/schemas/SpotifyExternalUrls" }, "href": { "type": "string", "description": "A link to the Web API endpoint providing full details" }, "id": { "type": "string", "description": "The Spotify ID for the artist" }, "name": { "type": "string", "description": "The name of the artist" }, "type": { "type": "string", "enum": [ "artist" ], "description": "The object type, always 'artist'" }, "uri": { "type": "string", "description": "The Spotify URI for the artist" } } }, "SpotifyAlbumTracks": { "type": "object", "properties": { "href": { "type": "string", "description": "A link to the Web API endpoint" }, "limit": { "type": "integer", "description": "The maximum number of items in the response" }, "next": { "type": "string", "nullable": true, "description": "URL to the next page of items" }, "offset": { "type": "integer", "description": "The offset of the items returned" }, "previous": { "type": "string", "nullable": true, "description": "URL to the previous page" }, "total": { "type": "integer", "description": "Total number of items available" }, "items": { "type": "array", "items": { "$ref": "#/components/schemas/SpotifySimplifiedTrack" }, "description": "Array of simplified track objects" } } }, "SpotifyCopyright": { "type": "object", "properties": { "text": { "type": "string", "description": "The copyright text" }, "type": { "type": "string", "description": "The type of copyright" } } }, "SpotifySimplifiedTrack": { "type": "object", "properties": { "artists": { "type": "array", "items": { "$ref": "#/components/schemas/SpotifySimplifiedArtist" }, "description": "The artists who performed the track" }, "available_markets": { "type": "array", "items": { "type": "string" }, "description": "Markets in which the track is available" }, "disc_number": { "type": "integer", "description": "Disc number the track is on" }, "duration_ms": { "type": "integer", "description": "Track length in milliseconds" }, "explicit": { "type": "boolean", "description": "Whether the track has explicit lyrics" }, "external_urls": { "$ref": "#/components/schemas/SpotifyExternalUrls" }, "href": { "type": "string", "description": "Link to the Web API endpoint" }, "id": { "type": "string", "description": "Spotify ID for the track" }, "is_playable": { "type": "boolean", "description": "If true, the track is playable" }, "linked_from": { "type": "object", "description": "Track relinking info" }, "restrictions": { "type": "object", "properties": { "reason": { "type": "string" } } }, "name": { "type": "string", "description": "Track name" }, "preview_url": { "type": "string", "nullable": true, "description": "URL to a 30 second preview" }, "track_number": { "type": "integer", "description": "Track number on the album" }, "type": { "type": "string", "enum": [ "track" ] }, "uri": { "type": "string", "description": "Spotify URI for the track" }, "is_local": { "type": "boolean", "description": "Whether from a local file" } } } } } } ``` # Songs by ISRC Source: https://recoupable.dev/docs/api-reference/apple/songs Look up recordings in the Apple Music catalog by ISRC. Accepts up to 25 comma-separated ISRCs per request and returns one row per **requested** ISRC, so a recording that Apple does not carry comes back explicitly as `found: false` rather than being omitted. Complements `GET /api/spotify/search`, which reaches ISRCs only through a fuzzy `isrc:` search query; Apple matches the identifier exactly and additionally returns release-level rights metadata (`upc`, `record_label`, `copyright`) that Spotify does not expose. ## GET /api/apple/songs Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/apple/songs": { "get": { "description": "Look up recordings in the Apple Music catalog by ISRC. Accepts up to 25 comma-separated ISRCs per request and returns one row per **requested** ISRC, so a recording that Apple does not carry comes back explicitly as `found: false` rather than being omitted. Complements `GET /api/spotify/search`, which reaches ISRCs only through a fuzzy `isrc:` search query; Apple matches the identifier exactly and additionally returns release-level rights metadata (`upc`, `record_label`, `copyright`) that Spotify does not expose.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "isrc", "in": "query", "description": "One or more ISRCs, comma-separated. Case-insensitive; each must match the 12-character ISRC format (two-letter country code, three-character registrant code, two-digit year, five-digit designation). Maximum 25 per request.", "required": true, "schema": { "type": "string", "example": "DEH742611917,TCAEC1931080" } }, { "name": "storefront", "in": "query", "description": "Apple Music storefront to search, as a two-letter country code. Availability is territory-specific, so a recording present in one storefront may be absent from another.", "required": false, "schema": { "type": "string", "default": "us", "example": "gb" } } ], "responses": { "200": { "description": "Lookup completed. One entry per requested ISRC, in the order requested.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AppleSongsResponse" } } } }, "400": { "description": "Bad request - `isrc` missing, malformed, or over the 25-ISRC limit, or an unknown `storefront`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AppleErrorResponse" }, "example": { "status": "error", "error": "isrc must be a valid ISRC: NOTANISRC" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AppleErrorResponse" }, "example": { "status": "error", "error": "Exactly one of x-api-key or Authorization must be provided" } } } }, "500": { "description": "Internal server error, including a failure to reach the Apple Music API", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AppleErrorResponse" }, "example": { "status": "error", "error": "Failed to reach the Apple Music API" } } } } } } } }, "components": { "schemas": { "AppleSongsResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "storefront": { "type": "string", "description": "The storefront the lookup ran against", "example": "us" }, "results": { "type": "array", "description": "One entry per requested ISRC, in the order requested", "items": { "$ref": "#/components/schemas/AppleIsrcResult" } } } }, "AppleErrorResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Human-readable error message" } } }, "AppleIsrcResult": { "type": "object", "properties": { "isrc": { "type": "string", "description": "The requested ISRC, uppercased", "example": "DEH742611917" }, "found": { "type": "boolean", "description": "Whether Apple Music carries at least one recording with this ISRC in the requested storefront", "example": true }, "songs": { "type": "array", "description": "Every recording Apple returns for this ISRC. Usually one, but the same recording can appear on several releases (compilations, anniversary editions), each with its own song id.", "items": { "$ref": "#/components/schemas/AppleSong" } } } }, "AppleSong": { "type": "object", "properties": { "id": { "type": "string", "description": "Apple Music catalog song id", "example": "1894880802" }, "isrc": { "type": "string", "example": "DEH742611917" }, "name": { "type": "string", "example": "Steady Box Fan White Noise (Extended Mix)" }, "artist_name": { "type": "string", "description": "Display credit as Apple renders it, which may combine several artists", "example": "Sleep Sounds & Sleepy Buddy" }, "composer_name": { "type": "string", "nullable": true, "example": "Zachary Kubilus" }, "album_name": { "type": "string", "example": "Box Fan All Night" }, "release_date": { "type": "string", "format": "date", "nullable": true, "example": "2026-05-01" }, "duration_ms": { "type": "integer", "example": 124001 }, "track_number": { "type": "integer", "nullable": true, "example": 6 }, "disc_number": { "type": "integer", "nullable": true, "example": 1 }, "genre_names": { "type": "array", "items": { "type": "string" }, "example": [ "New Age", "Music", "Worldwide" ] }, "has_lyrics": { "type": "boolean", "example": false }, "is_apple_digital_master": { "type": "boolean", "example": false }, "audio_variants": { "type": "array", "description": "Delivery formats available, for example `lossless`, `lossy-stereo`, `dolby-atmos`", "items": { "type": "string" }, "example": [ "lossless", "lossy-stereo" ] }, "url": { "type": "string", "description": "Public Apple Music page for the recording", "example": "https://music.apple.com/us/album/steady-box-fan-white-noise-extended-mix/1894880796?i=1894880802" }, "artwork_url": { "type": "string", "nullable": true, "description": "Artwork template URL. Substitute `{w}` and `{h}` with the pixel dimensions you want.", "example": "https://is1-ssl.mzstatic.com/image/thumb/Music221/v4/d5/d1/12/d5d112cd-19f8-6cf7-b048-b2f94c75fefd/4065328882161.png/{w}x{h}bb.jpg" }, "preview_url": { "type": "string", "nullable": true, "description": "30-second AAC preview", "example": "https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview211/v4/56/64/02/56640222-1893-38a9-e25f-9869aa117282/mzaf_15389777242743919344.plus.aac.p.m4a" }, "album": { "$ref": "#/components/schemas/AppleSongAlbum" } } }, "AppleSongAlbum": { "type": "object", "nullable": true, "description": "The release this recording appears on. Carries the rights metadata Apple exposes and Spotify does not.", "properties": { "id": { "type": "string", "example": "1894880796" }, "name": { "type": "string", "example": "Box Fan All Night" }, "upc": { "type": "string", "nullable": true, "example": "4065328882161" }, "record_label": { "type": "string", "nullable": true, "example": "Sleep Sounds" }, "copyright": { "type": "string", "nullable": true, "description": "The phonographic copyright line", "example": "℗ 2026 Sleep Sounds" }, "release_date": { "type": "string", "format": "date", "nullable": true, "example": "2026-05-01" }, "track_count": { "type": "integer", "nullable": true, "example": 20 }, "is_single": { "type": "boolean", "example": false }, "is_compilation": { "type": "boolean", "example": false }, "is_complete": { "type": "boolean", "description": "Whether every track on the release is available in this storefront", "example": true }, "url": { "type": "string", "example": "https://music.apple.com/us/album/box-fan-all-night/1894880796" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Scraper Results Source: https://recoupable.dev/docs/api-reference/apify/scraper Check the status and retrieve results from Apify scraper runs. This endpoint uses the Apify API Client to fetch the current status of a scraper run and its results if available. Use the runId returned from endpoints like Instagram Comments, Instagram Profiles, Social Scrape, or Artist Socials Scrape to poll for results. ## GET /api/apify/runs/{runId} Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/apify/runs/{runId}": { "get": { "description": "Check the status and retrieve results from Apify scraper runs. This endpoint uses the Apify API Client to fetch the current status of a scraper run and its results if available. Use the runId returned from endpoints like Instagram Comments, Instagram Profiles, Social Scrape, or Artist Socials Scrape to poll for results.", "parameters": [ { "name": "runId", "in": "path", "description": "The ID of the Apify run to check status for. This is returned when starting a scrape via Instagram Comments, Instagram Profiles, Social Scrape, or Artist Socials Scrape endpoints.", "required": true, "schema": { "type": "string" }, "example": "abc123xyz" } ], "responses": { "200": { "description": "Scraper run status retrieved successfully. Returns status info for in-progress runs, or status info plus data for completed runs.", "content": { "application/json": { "schema": { "oneOf": [ { "$ref": "#/components/schemas/ApifyScraperInProgressResponse" }, { "$ref": "#/components/schemas/ApifyScraperCompletedResponse" } ] } } } }, "400": { "description": "Bad request - missing required runId parameter", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApifyScraperErrorResponse" } } } }, "500": { "description": "Server error - failed to fetch run status from Apify", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApifyScraperErrorResponse" } } } } } } } }, "components": { "schemas": { "ApifyScraperInProgressResponse": { "type": "object", "required": [ "status", "dataset_id" ], "description": "Response when the Apify run is still in progress", "properties": { "status": { "type": "string", "description": "Current status of the Apify run (e.g., 'RUNNING', 'READY')", "example": "RUNNING" }, "dataset_id": { "type": "string", "description": "ID of the dataset that will contain the results when the run completes", "example": "def456uvw", "nullable": true } } }, "ApifyScraperCompletedResponse": { "type": "object", "required": [ "status", "dataset_id", "data" ], "description": "Response when the Apify run has completed successfully", "properties": { "status": { "type": "string", "description": "Final status of the Apify run (typically 'SUCCEEDED')", "example": "SUCCEEDED" }, "dataset_id": { "type": "string", "description": "ID of the dataset containing the results", "example": "def456uvw" }, "data": { "type": "array", "items": { "type": "object", "additionalProperties": true }, "description": "Array of dataset items returned by the scraper. The structure of each item varies depending on the scraper type.", "example": [ { "id": "123456789", "username": "example_user", "fullName": "Example User", "biography": "This is a sample biography", "followersCount": 1000, "followsCount": 500, "profilePicUrl": "https://example.com/profile.jpg", "latestPosts": [ { "shortCode": "AbCdEfGhIjK", "type": "Video", "caption": "Example reel caption", "likesCount": 16, "commentsCount": 4, "videoViewCount": 56, "timestamp": "2026-06-30T00:26:41.000Z" } ] }, { "type": "tweet", "id": "1234567890123456789", "url": "https://x.com/example_user/status/1234567890123456789", "text": "Example tweet text", "createdAt": "Wed Jul 01 17:38:08 +0000 2026", "likeCount": 7, "retweetCount": 2, "replyCount": 1, "quoteCount": 0, "bookmarkCount": 1, "viewCount": 258 }, { "type": "video", "id": "aBcDeFgHiJk", "title": "Example Short", "url": "https://www.youtube.com/watch?v=aBcDeFgHiJk", "date": "2026-07-01T17:55:00.000Z", "viewCount": 26, "likes": 1, "commentsCount": 0, "duration": "00:00:45", "channelUsername": "examplechannel" } ] } } }, "ApifyScraperErrorResponse": { "type": "object", "required": [ "status", "message" ], "description": "Error response from the Apify scraper results endpoint", "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status indicating an error occurred" }, "message": { "type": "string", "description": "Error message describing what went wrong", "example": "runId is required" } } } } } } ``` # Agent Signup Source: https://recoupable.dev/docs/api-reference/agents/signup Register an agent. For new agent+ emails, returns an API key immediately. For all other cases, sends a verification code to the email — call [POST /api/agents/verify](/docs/api-reference/agents/verify) with the code to get your API key. ## POST /api/agents/signup Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation's specification permits a request without authentication. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/agents/signup": { "post": { "description": "Register an agent. For new agent+ emails, returns an API key immediately. For all other cases, sends a verification code to the email — call [POST /api/agents/verify](/docs/api-reference/agents/verify) with the code to get your API key.\n\n**Tip:** If you're unsure what email to register, generate a unique `agent+{suffix}@recoupable.com` address (e.g. `agent+1744410896-28439@recoupable.com`, combining a Unix timestamp with a random integer). This guarantees a fresh `agent+` address on every call — including multiple signups within the same second — and returns an API key instantly with no email verification required.", "security": [], "requestBody": { "description": "Agent email to register", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AgentSignupRequest" } } } }, "responses": { "200": { "description": "Signup processed successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AgentSignupResponse" } } } } } } } }, "components": { "schemas": { "AgentSignupRequest": { "type": "object", "required": [ "email" ], "properties": { "email": { "type": "string", "format": "email", "description": "The agent email address. Emails with the agent+ prefix (e.g. agent+mybot@example.com) get an API key immediately on first signup.", "example": "agent+mybot@example.com" } } }, "AgentSignupResponse": { "type": "object", "required": [ "account_id", "message" ], "properties": { "account_id": { "type": "string", "format": "uuid", "description": "The account ID for the registered agent.", "example": "123e4567-e89b-12d3-a456-426614174000" }, "api_key": { "type": [ "string", "null" ], "description": "API key returned immediately for new agent+ prefix emails. Null for all other cases — check your email for a verification code.", "example": "recoup_sk_abc123..." }, "message": { "type": "string", "description": "Human-readable status message.", "example": "If this is a new agent+ email, your API key is included. Otherwise, check your email for a verification code." } } } } } } ``` # Verify Agent Email Source: https://recoupable.dev/docs/api-reference/agents/verify Verify an agent's email with the code sent during signup. Returns an API key on success. ## POST /api/agents/verify Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation's specification permits a request without authentication. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/agents/verify": { "post": { "description": "Verify an agent's email with the code sent during signup. Returns an API key on success.", "security": [], "requestBody": { "description": "Email and verification code", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AgentVerifyRequest" } } } }, "responses": { "200": { "description": "Verification successful", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AgentVerifyResponse" } } } }, "400": { "description": "Invalid or expired verification code", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Too many failed verification attempts", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "AgentVerifyRequest": { "type": "object", "required": [ "email", "code" ], "properties": { "email": { "type": "string", "format": "email", "description": "The email address used during signup.", "example": "you@example.com" }, "code": { "type": "string", "minLength": 6, "maxLength": 6, "pattern": "^[0-9]{6}$", "description": "The 6-digit verification code sent to the email.", "example": "123456" } } }, "AgentVerifyResponse": { "type": "object", "required": [ "account_id", "api_key", "message" ], "properties": { "account_id": { "type": "string", "format": "uuid", "description": "The account ID for the verified agent.", "example": "123e4567-e89b-12d3-a456-426614174000" }, "api_key": { "type": "string", "description": "API key for the verified account.", "example": "recoup_sk_abc123..." }, "message": { "type": "string", "description": "Human-readable status message.", "example": "Verified" } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } } } } } ``` # Agents Source: https://recoupable.dev/docs/agents Verified email onboarding for a separate Recoup REST API key. ## Use your real account Prefer [MCP OAuth](https://recoupable.dev/docs/mcp-oauth) for supported AI hosts. REST integrations need a separate API key; an OAuth connection does not export one into your shell. Use the email verification flow below with an address the user controls. Keep the returned key out of messages and logs, and store it through a secret facility or an explicitly approved local environment file. Never use a disposable account to recover an existing user's data. The source signup contract also supports immediate keys for new `agent+` addresses. That creates a separate identity; it does not grant customer or organization access. It is not the recommended onboarding path for customer work. Multiple API keys per account are supported, without revoking existing keys. ## Standard signup (email verification) If you're building a human-facing integration and want the user to verify their real email, use an email address the user controls: Step 1 — request a verification code: ```bash curl -X POST "https://api.recoupable.dev/api/agents/signup" \ -H "Content-Type: application/json" \ -d '{"email": "you@example.com"}' ``` Step 2 — submit the 6-digit code from the verification email: ```bash curl -X POST "https://api.recoupable.dev/api/agents/verify" \ -H "Content-Type: application/json" \ -d '{"email": "you@example.com", "code": "123456"}' ``` Response: ```json { "account_id": "123e4567-e89b-12d3-a456-426614174000", "api_key": "recoup_sk_abc123...", "message": "Verified" } ``` ## Using your API key Pass the returned `api_key` in the `x-api-key` header on every authenticated request: ```bash curl -X GET "https://api.recoupable.dev/api/tasks" \ -H "x-api-key: YOUR_API_KEY" ``` See [Authentication](https://recoupable.dev/docs/authentication) for the full authentication model, including organization access and Bearer token support, and [Quickstart](https://recoupable.dev/docs/quickstart) for your first end-to-end request. # Content Agent Source: https://recoupable.dev/docs/content-agent Use hosted creative presets and current image, video and composition primitives. ## Hosted workflows `POST /api/content/create` retains the hosted preset workflow. Supply the authorized `artist_account_id`, optional `template`, `images`, `songs`, `batch` (defaults to 1; range 1–30), `lipsync`, `upscale` and `caption_length`. Discover templates through `/api/content/templates` and its detail route. The response contains `runIds`; poll `/api/tasks/runs?runId=...` until terminal. Report failures and inspect the final artifact before delivery. Hosted lipsync is a supported preset intent. The coordinated API/worker repair restores the selected song window to the fixed audio-to-video path; that repaired journey is release-pending until deployment and actual output verification. ## Direct primitives For images use `POST /api/content/image` with `prompt` and up to 10 unique `image_urls`. The house model is Muse Image, switching to its edit path when references are present. New clients must use `image_urls`; compatibility aliases are translated by the reconciliation release. For video use `POST /api/content/video` with `prompt`, optional `image_url` and `end_image_url`, integer `duration` 5–15, and `resolution` `480P` or `768P`. The house model is MiniMax H3 Max. It has no six-mode selector, arbitrary model, reference-only, extend, `video_url` or `audio_url` request control. Unsupported semantic inputs return 400 in the reconciliation release, before paid generation. The dedicated `/api/content/lipsync` primitive restores the existing fixed LTX model for the worker; it is not assumed available on an older deployment. Caption, transcribe, analyze, upscale and edit have their own endpoint contracts. Local ffmpeg composition can crop, trim, overlay or mux a song; it does not create mouth synchronization or prove a hosted generation succeeded. ## Cost and evidence Each generation/inference stage can consume credits. `/api/content/estimate` returns an advisory estimate, not a guaranteed price or complete final ledger. A later failed composition does not imply that completed upstream work was free. On insufficient credits, surface `billingUrl` and stop. Consult [Credits](https://recoupable.dev/docs/credits). Record image inspection, motion review and audio playback. The worker checks for an audio stream on lipsync output before composition; perceptual mouth alignment still requires inspection. Automated checks do not replace human QA. ## Slack integration The separate [Slack webhook](https://recoupable.dev/docs/api-reference/content-agent/webhook) handles Content Agent mentions. Its request/event contract and the [completion callback](https://recoupable.dev/docs/api-reference/content-agent/callback) are distinct from direct primitives. Workspace operators must configure the Slack application and service credentials; installing a creative skill does not install that integration. ### Set up the Slack app Create a Slack app, enable the `chat:write` and `app_mentions:read` bot scopes, and subscribe to `app_mention` events at `https://api.recoupable.dev/api/content-agent/slack`. Install it in the intended workspace and invite it to the channel. Configure `SLACK_CONTENT_BOT_TOKEN`, `SLACK_CONTENT_SIGNING_SECRET`, `CODING_AGENT_CALLBACK_SECRET` and `REDIS_URL` in the Recoup API service. The existing Tasks worker uses the same `CODING_AGENT_CALLBACK_SECRET` for completion callbacks and its authorized Recoup credential and API base URL. Keep these values in service secret settings. The current callback secret name is `CODING_AGENT_CALLBACK_SECRET`; `CONTENT_AGENT_CALLBACK_SECRET` is obsolete. Mention the installed bot with the authorized artist account UUID: ```text @RecoupContentAgent ARTIST_ACCOUNT_UUID artist-caption-bedroom batch=1 ``` The template, `batch=N` (1–30) and `lipsync` flags are optional. Check the acknowledgment, then inspect the completed result or reported failure in the thread. Generated output still requires the inspection described above. # List Templates Source: https://recoupable.dev/docs/api-reference/templates/list List templates ## GET /api/agents/templates Full OpenAPI specification: https://recoupable.dev/docs/spec/templates.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Templates", "description": "API documentation for managing reusable templates on the Recoup platform. Templates capture a prompt, description, and tags that can be shared with other accounts or kept private.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "paths": { "/api/agents/templates": { "get": { "summary": "List templates", "description": "Retrieve every template visible to the authenticated account. The response combines templates the account owns, public templates created by other accounts, and private templates that have been shared with the authenticated account's email.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Templates retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ListTemplatesResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } } }, "components": { "schemas": { "ListTemplatesResponse": { "type": "object", "required": [ "status", "templates" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "templates": { "type": "array", "items": { "$ref": "#/components/schemas/Template" }, "description": "Templates accessible to the authenticated account (own, public, and shared)" } } }, "ErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Human-readable error message" }, "missing_fields": { "type": "array", "items": { "type": "string" }, "description": "Names of required fields that were missing from the request, when applicable" } } }, "Template": { "type": "object", "required": [ "id", "title", "description", "prompt", "tags", "creator", "is_private", "is_favourite", "favorites_count", "shared_emails", "created_at", "updated_at" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier of the template" }, "title": { "type": "string", "description": "Short human-readable title for the template (3-50 characters)" }, "description": { "type": "string", "description": "Description of what the template does (10-200 characters)" }, "prompt": { "type": "string", "description": "The agent prompt body (20-10000 characters)" }, "tags": { "type": "array", "items": { "type": "string" }, "description": "Free-form tags used to categorize the template. Empty array when there are no tags." }, "creator": { "$ref": "#/components/schemas/TemplateCreator" }, "is_private": { "type": "boolean", "description": "Whether the template is restricted to the creator and explicitly shared accounts" }, "is_favourite": { "type": "boolean", "description": "Whether the authenticated account has favorited this template" }, "favorites_count": { "type": "integer", "description": "Total number of accounts that have favorited this template" }, "shared_emails": { "type": "array", "items": { "type": "string", "format": "email" }, "description": "Emails the template has been shared with. Only populated for private templates the authenticated account owns; empty for public templates and for private templates shared with the caller." }, "created_at": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp of when the template was created" }, "updated_at": { "type": [ "string", "null" ], "format": "date-time", "description": "ISO 8601 timestamp of when the template was last updated" } } }, "TemplateCreator": { "type": [ "object", "null" ], "description": "The account that created the template. May be null when the creator account is unavailable.", "required": [ "id", "name", "image", "is_admin" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier of the creator account" }, "name": { "type": [ "string", "null" ], "description": "Display name of the creator account" }, "image": { "type": [ "string", "null" ], "description": "Profile image URL of the creator account" }, "is_admin": { "type": "boolean", "description": "Whether the creator is a Recoup admin (their account email is on the platform admin allow-list)" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Create Template Source: https://recoupable.dev/docs/api-reference/templates/create Create a template ## POST /api/agents/templates Full OpenAPI specification: https://recoupable.dev/docs/spec/templates.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Templates", "description": "API documentation for managing reusable templates on the Recoup platform. Templates capture a prompt, description, and tags that can be shared with other accounts or kept private.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "paths": { "/api/agents/templates": { "post": { "summary": "Create a template", "description": "Create a new template owned by the authenticated account. When `is_private` is true, the optional `share_emails` array grants explicit read access to the listed accounts; for public templates the field is ignored.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Template fields", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateTemplateRequest" } } } }, "responses": { "201": { "description": "Template created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TemplateResponse" } } } }, "400": { "description": "Bad request - validation failed or required fields missing", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } } }, "components": { "schemas": { "CreateTemplateRequest": { "type": "object", "required": [ "title", "description", "prompt", "tags", "is_private" ], "properties": { "title": { "type": "string", "minLength": 3, "maxLength": 50, "description": "Short human-readable title for the template" }, "description": { "type": "string", "minLength": 10, "maxLength": 200, "description": "Description of what the template does" }, "prompt": { "type": "string", "minLength": 20, "maxLength": 10000, "description": "The agent prompt body" }, "tags": { "type": "array", "items": { "type": "string" }, "description": "Free-form tags used to categorize the template. Pass an empty array if there are no tags." }, "is_private": { "type": "boolean", "description": "When true, the template is restricted to the creator and the accounts listed in `share_emails`" }, "share_emails": { "type": "array", "items": { "type": "string", "format": "email" }, "description": "Emails to grant access to. Only applied when `is_private` is true. Defaults to an empty array when omitted." } } }, "TemplateResponse": { "type": "object", "required": [ "status", "template" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "template": { "$ref": "#/components/schemas/Template" } } }, "ErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Human-readable error message" }, "missing_fields": { "type": "array", "items": { "type": "string" }, "description": "Names of required fields that were missing from the request, when applicable" } } }, "Template": { "type": "object", "required": [ "id", "title", "description", "prompt", "tags", "creator", "is_private", "is_favourite", "favorites_count", "shared_emails", "created_at", "updated_at" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier of the template" }, "title": { "type": "string", "description": "Short human-readable title for the template (3-50 characters)" }, "description": { "type": "string", "description": "Description of what the template does (10-200 characters)" }, "prompt": { "type": "string", "description": "The agent prompt body (20-10000 characters)" }, "tags": { "type": "array", "items": { "type": "string" }, "description": "Free-form tags used to categorize the template. Empty array when there are no tags." }, "creator": { "$ref": "#/components/schemas/TemplateCreator" }, "is_private": { "type": "boolean", "description": "Whether the template is restricted to the creator and explicitly shared accounts" }, "is_favourite": { "type": "boolean", "description": "Whether the authenticated account has favorited this template" }, "favorites_count": { "type": "integer", "description": "Total number of accounts that have favorited this template" }, "shared_emails": { "type": "array", "items": { "type": "string", "format": "email" }, "description": "Emails the template has been shared with. Only populated for private templates the authenticated account owns; empty for public templates and for private templates shared with the caller." }, "created_at": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp of when the template was created" }, "updated_at": { "type": [ "string", "null" ], "format": "date-time", "description": "ISO 8601 timestamp of when the template was last updated" } } }, "TemplateCreator": { "type": [ "object", "null" ], "description": "The account that created the template. May be null when the creator account is unavailable.", "required": [ "id", "name", "image", "is_admin" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier of the creator account" }, "name": { "type": [ "string", "null" ], "description": "Display name of the creator account" }, "image": { "type": [ "string", "null" ], "description": "Profile image URL of the creator account" }, "is_admin": { "type": "boolean", "description": "Whether the creator is a Recoup admin (their account email is on the platform admin allow-list)" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Update Template Source: https://recoupable.dev/docs/api-reference/templates/update Update a template ## PATCH /api/agents/templates/{id} Full OpenAPI specification: https://recoupable.dev/docs/spec/templates.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Templates", "description": "API documentation for managing reusable templates on the Recoup platform. Templates capture a prompt, description, and tags that can be shared with other accounts or kept private.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "paths": { "/api/agents/templates/{id}": { "patch": { "summary": "Update a template", "description": "Update fields on an existing template owned by the authenticated account. All body fields are optional and only the supplied fields are modified. Providing `share_emails` replaces the existing share list for the template.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier (UUID) of the template to update", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "description": "Fields to update on the template", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateTemplateRequest" } } } }, "responses": { "200": { "description": "Template updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TemplateResponse" } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Forbidden - the authenticated account does not own this template", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Template not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } } }, "components": { "schemas": { "UpdateTemplateRequest": { "type": "object", "description": "All fields are optional. Only supplied fields are updated.", "properties": { "title": { "type": "string", "minLength": 3, "maxLength": 50, "description": "Short human-readable title for the template" }, "description": { "type": "string", "minLength": 10, "maxLength": 200, "description": "Description of what the template does" }, "prompt": { "type": "string", "minLength": 20, "maxLength": 10000, "description": "The agent prompt body" }, "tags": { "type": "array", "items": { "type": "string" }, "description": "Free-form tags used to categorize the template. Pass an empty array to clear tags." }, "is_private": { "type": "boolean", "description": "When true, the template is restricted to the creator and the accounts listed in `share_emails`" }, "share_emails": { "type": "array", "items": { "type": "string", "format": "email" }, "description": "Replaces the existing share list for the template. Only applied when the template is (or becomes) private." } } }, "TemplateResponse": { "type": "object", "required": [ "status", "template" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "template": { "$ref": "#/components/schemas/Template" } } }, "ErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Human-readable error message" }, "missing_fields": { "type": "array", "items": { "type": "string" }, "description": "Names of required fields that were missing from the request, when applicable" } } }, "Template": { "type": "object", "required": [ "id", "title", "description", "prompt", "tags", "creator", "is_private", "is_favourite", "favorites_count", "shared_emails", "created_at", "updated_at" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier of the template" }, "title": { "type": "string", "description": "Short human-readable title for the template (3-50 characters)" }, "description": { "type": "string", "description": "Description of what the template does (10-200 characters)" }, "prompt": { "type": "string", "description": "The agent prompt body (20-10000 characters)" }, "tags": { "type": "array", "items": { "type": "string" }, "description": "Free-form tags used to categorize the template. Empty array when there are no tags." }, "creator": { "$ref": "#/components/schemas/TemplateCreator" }, "is_private": { "type": "boolean", "description": "Whether the template is restricted to the creator and explicitly shared accounts" }, "is_favourite": { "type": "boolean", "description": "Whether the authenticated account has favorited this template" }, "favorites_count": { "type": "integer", "description": "Total number of accounts that have favorited this template" }, "shared_emails": { "type": "array", "items": { "type": "string", "format": "email" }, "description": "Emails the template has been shared with. Only populated for private templates the authenticated account owns; empty for public templates and for private templates shared with the caller." }, "created_at": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp of when the template was created" }, "updated_at": { "type": [ "string", "null" ], "format": "date-time", "description": "ISO 8601 timestamp of when the template was last updated" } } }, "TemplateCreator": { "type": [ "object", "null" ], "description": "The account that created the template. May be null when the creator account is unavailable.", "required": [ "id", "name", "image", "is_admin" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier of the creator account" }, "name": { "type": [ "string", "null" ], "description": "Display name of the creator account" }, "image": { "type": [ "string", "null" ], "description": "Profile image URL of the creator account" }, "is_admin": { "type": "boolean", "description": "Whether the creator is a Recoup admin (their account email is on the platform admin allow-list)" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Delete Template Source: https://recoupable.dev/docs/api-reference/templates/delete Delete a template ## DELETE /api/agents/templates/{id} Full OpenAPI specification: https://recoupable.dev/docs/spec/templates.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Templates", "description": "API documentation for managing reusable templates on the Recoup platform. Templates capture a prompt, description, and tags that can be shared with other accounts or kept private.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "paths": { "/api/agents/templates/{id}": { "delete": { "summary": "Delete a template", "description": "Permanently delete an template. Only the owner of the template may perform this action. Associated share records and favorite entries are removed as part of the deletion.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier (UUID) of the template to delete", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Template deleted successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuccessResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Forbidden - the authenticated account does not own this template", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Template not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } } }, "components": { "schemas": { "SuccessResponse": { "type": "object", "required": [ "status" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" } } }, "ErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Human-readable error message" }, "missing_fields": { "type": "array", "items": { "type": "string" }, "description": "Names of required fields that were missing from the request, when applicable" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Favorite Template Source: https://recoupable.dev/docs/api-reference/templates/favorite Toggle favorite on a template ## PUT /api/agents/templates/{id}/favorite Full OpenAPI specification: https://recoupable.dev/docs/spec/templates.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Templates", "description": "API documentation for managing reusable templates on the Recoup platform. Templates capture a prompt, description, and tags that can be shared with other accounts or kept private.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "paths": { "/api/agents/templates/{id}/favorite": { "put": { "summary": "Toggle favorite on a template", "description": "Mark or unmark an template as a favorite for the authenticated account. The endpoint is idempotent - calling it repeatedly with the same `is_favourite` value has no additional effect. The caller must be able to see the template (own it, the template is public, or it has been shared with them).", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier (UUID) of the template to favorite or unfavorite", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "description": "Desired favorite state", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ToggleFavoriteRequest" } } } }, "responses": { "200": { "description": "Favorite state updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuccessResponse" } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Forbidden - the template is private and has not been shared with the authenticated account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Template not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } } }, "components": { "schemas": { "ToggleFavoriteRequest": { "type": "object", "required": [ "is_favourite" ], "properties": { "is_favourite": { "type": "boolean", "description": "Desired favorite state for the authenticated account" } } }, "SuccessResponse": { "type": "object", "required": [ "status" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" } } }, "ErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Human-readable error message" }, "missing_fields": { "type": "array", "items": { "type": "string" }, "description": "Names of required fields that were missing from the request, when applicable" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Tasks Source: https://recoupable.dev/docs/api-reference/tasks/get Retrieve scheduled tasks. Each task includes `recent_runs` (last 5 runs), `upcoming` (next scheduled run times) sourced directly from the Trigger.dev API, and `owner_email` when an account email exists for the task owner. Supports filtering by id, account_id, or artist_account_id. ## GET /api/tasks Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/tasks": { "get": { "description": "Retrieve scheduled tasks. Each task includes `recent_runs` (last 5 runs), `upcoming` (next scheduled run times) sourced directly from the Trigger.dev API, and `owner_email` when an account email exists for the task owner. Supports filtering by id, account_id, or artist_account_id.\n\nFetching one task by id performs the owner-plan preflight used by the runner and can return 402 plan_limit. Listing tasks remains distinct from permission to execute them.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "id", "in": "query", "description": "Filter by task ID (UUID). Returns a single task matching the provided ID. Admin callers may retrieve any task by ID regardless of owner; non-admin callers only receive the task if it belongs to their authenticated account.", "required": false, "schema": { "type": "string", "format": "uuid" } }, { "name": "account_id", "in": "query", "description": "Filter tasks to only include those for the specified account.", "required": false, "schema": { "type": "string", "format": "uuid" } }, { "name": "artist_account_id", "in": "query", "description": "Filter tasks to only include those for the specified artist account.", "required": false, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Tasks retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TasksResponse" } } } }, "400": { "description": "Bad request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "A single task fetched by id is blocked by its owner plan. Returns plan_limit with task_count or min_cadence and billingUrl. Ordinary listing does not apply this pre-run gate.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlanLimitError" } } } }, "403": { "description": "Forbidden - account_id is outside caller authorization scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal server error while retrieving tasks.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "TasksResponse": { "type": "object", "required": [ "status", "tasks" ], "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "tasks": { "type": "array", "items": { "$ref": "#/components/schemas/Task" }, "description": "Array of task objects" }, "error": { "type": "string", "description": "Error message (only present if status is error)" } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } }, "PlanLimitError": { "type": "object", "required": [ "status", "error", "limit", "message", "plan", "task_limit", "min_cadence_minutes", "current_task_count", "billingUrl" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `error` for this response shape" }, "error": { "type": "string", "enum": [ "plan_limit" ], "description": "Machine-readable code. Literally `plan_limit`." }, "limit": { "type": "string", "enum": [ "task_count", "min_cadence" ], "description": "Which entitlement blocked the request: `task_count` (the plan's enabled-task cap is reached) or `min_cadence` (the cron fires more often than the plan allows)." }, "message": { "type": "string", "description": "Human-readable explanation naming the plan and the limit, suitable to show to a user.", "example": "Free includes 1 task. Starter includes 3, Pro is unlimited." }, "plan": { "type": "string", "enum": [ "free", "starter", "pro" ], "description": "The account's current plan, resolved from its subscription." }, "task_limit": { "type": "integer", "nullable": true, "minimum": 1, "description": "Maximum enabled tasks on the current plan. Null means uncapped.", "example": 1 }, "min_cadence_minutes": { "type": "integer", "minimum": 1, "description": "Shortest allowed gap between two consecutive runs of a task on the current plan, in minutes (Free 10080, Starter 1440, Pro 60).", "example": 10080 }, "current_task_count": { "type": "integer", "minimum": 0, "description": "Enabled tasks the account has right now, excluding the one being created or updated.", "example": 1 }, "billingUrl": { "type": "string", "format": "uri", "description": "Static link to `/plan` in the Recoup app, where a human can upgrade. Constant; retrying creates nothing.", "example": "https://app.recoupable.dev/plan" } } }, "Task": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the task" }, "title": { "type": "string", "description": "Descriptive title or name of the task" }, "prompt": { "type": "string", "description": "Detailed instruction or prompt for task execution" }, "schedule": { "type": "string", "description": "Cron expression defining when the task should execute (e.g., '0 10 * * *')" }, "timezone": { "type": "string", "nullable": true, "description": "IANA time zone the task's cron is interpreted in, read from the Trigger.dev schedule (the source of truth). Null when the task has no schedule or the timezone can't be read.", "example": "America/New_York" }, "account_id": { "type": "string", "format": "uuid", "description": "Unique identifier for the associated account" }, "artist_account_id": { "type": "string", "format": "uuid", "description": "Unique identifier for the associated artist account" }, "enabled": { "type": "boolean", "nullable": true, "description": "Whether the task is enabled. Defaults to true." }, "trigger_schedule_id": { "type": "string", "nullable": true, "description": "Identifier for the trigger schedule associated with this task" }, "recent_runs": { "type": "array", "items": { "$ref": "#/components/schemas/TaskRunResponse" }, "description": "Last 5 runs for this task, sourced from the Trigger.dev API." }, "upcoming": { "type": "array", "items": { "type": "string", "format": "date-time" }, "description": "Next scheduled run times." }, "owner_email": { "type": "string", "nullable": true, "description": "Primary email address for the task owner account when one exists." }, "artist_name": { "type": "string", "nullable": true, "description": "Display name of the artist account the task runs for (`artist_account_id`), so a task list can label rows without a roster lookup. Null when the artist account no longer exists.", "example": "Braden Bales" }, "model": { "type": "string", "nullable": true, "minLength": 1, "description": "AI model identifier used when the task runs. When present as a string, must be non-empty; otherwise `null` if unset." } } }, "TaskRunResponse": { "type": "object", "description": "Raw Trigger.dev SDK run object. The API passes through the SDK response without field mapping. See https://trigger.dev/docs/management/runs/retrieve for the full reference. When listing runs, `output`, `error`, `payload`, and `attempts` are not included.", "required": [ "id", "status", "taskIdentifier", "createdAt", "updatedAt" ], "properties": { "id": { "type": "string", "description": "The unique run identifier, prefixed with `run_`" }, "status": { "type": "string", "enum": [ "PENDING_VERSION", "DELAYED", "QUEUED", "EXECUTING", "REATTEMPTING", "FROZEN", "COMPLETED", "CANCELED", "FAILED", "CRASHED", "INTERRUPTED", "SYSTEM_FAILURE" ], "description": "Current run status" }, "taskIdentifier": { "type": "string", "description": "The task type identifier (e.g. 'setup-sandbox', 'run-sandbox-command')" }, "idempotencyKey": { "type": "string", "nullable": true, "description": "Idempotency key used to deduplicate trigger requests" }, "version": { "type": "string", "description": "The worker version that executed the run" }, "isTest": { "type": "boolean", "description": "Whether this is a test run" }, "createdAt": { "type": "string", "format": "date-time", "description": "When the run was created (ISO 8601)" }, "updatedAt": { "type": "string", "format": "date-time", "description": "When the run was last updated (ISO 8601)" }, "startedAt": { "type": "string", "format": "date-time", "nullable": true, "description": "When execution started (null if not yet started)" }, "finishedAt": { "type": "string", "format": "date-time", "nullable": true, "description": "When the run finished (null if still running)" }, "delayedUntil": { "type": "string", "format": "date-time", "nullable": true, "description": "If delayed, when the run becomes eligible to execute" }, "ttl": { "description": "Time-to-live. If the run is not started within this duration, it expires.", "nullable": true }, "expiredAt": { "type": "string", "format": "date-time", "nullable": true, "description": "When the run expired (null if not expired)" }, "tags": { "type": "array", "items": { "type": "string" }, "description": "Tags associated with this run (max 10)" }, "metadata": { "type": "object", "nullable": true, "description": "JSON metadata attached to the run" }, "costInCents": { "type": "number", "description": "Compute cost of the run in cents" }, "baseCostInCents": { "type": "number", "description": "Base invocation cost in cents" }, "durationMs": { "type": "number", "description": "Compute duration in milliseconds" }, "env": { "type": "object", "description": "Environment the run executed in", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "user": { "type": "string", "nullable": true } } }, "depth": { "type": "integer", "description": "Nesting depth for child runs" }, "batchId": { "type": "string", "nullable": true, "description": "Batch ID if triggered as part of a batch" }, "triggerFunction": { "type": "string", "enum": [ "trigger", "triggerAndWait", "batchTrigger", "batchTriggerAndWait" ], "description": "The function used to trigger this run" }, "payload": { "description": "Input payload for the task. Only present when retrieving by runId.", "nullable": true }, "output": { "description": "Task output data. Only present when retrieving by runId, and only populated when `status` is `COMPLETED`. Shape varies by `taskIdentifier`. The `create-content` task returns the schema below; other tasks return their own.", "nullable": true, "oneOf": [ { "$ref": "#/components/schemas/CreateContentRunOutput" }, { "type": "object", "description": "Generic task output (for tasks not yet schema'd)" } ] }, "error": { "type": "object", "nullable": true, "description": "Error details if the run failed. Only present when retrieving by runId.", "properties": { "message": { "type": "string", "description": "Human-readable error message" }, "name": { "type": "string", "description": "Error name or type" }, "stackTrace": { "type": "string", "description": "Stack trace" } } }, "attempts": { "type": "array", "description": "Attempt history. Only present when retrieving by runId.", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Attempt ID, prefixed with `attempt_`" }, "status": { "type": "string", "enum": [ "PENDING", "EXECUTING", "PAUSED", "COMPLETED", "FAILED", "CANCELED" ] }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "startedAt": { "type": "string", "format": "date-time", "nullable": true }, "completedAt": { "type": "string", "format": "date-time", "nullable": true }, "error": { "type": "object", "nullable": true, "properties": { "message": { "type": "string" }, "name": { "type": "string" }, "stackTrace": { "type": "string" } } } } } }, "schedule": { "type": "object", "nullable": true, "description": "Schedule information if triggered by a schedule. Only present when retrieving by runId." }, "relatedRuns": { "type": "object", "nullable": true, "description": "Related run references (root, parent, children). Only present when retrieving by runId." } } }, "CreateContentRunOutput": { "type": "object", "description": "Output payload for a `create-content` task run. Returned in `output` when `status` is `COMPLETED`. Agents should poll `/api/tasks/runs?runId=…` and read these fields once the run is done.", "required": [ "videoSourceUrl", "imageUrl", "captionText", "template" ], "properties": { "videoSourceUrl": { "type": "string", "format": "uri", "description": "URL to the rendered final 9:16 video (image + motion + audio + caption already composed)." }, "imageUrl": { "type": "string", "format": "uri", "description": "URL to the base image used for video generation." }, "captionText": { "type": "string", "description": "The caption text burned into the video." }, "template": { "type": "string", "description": "Template id used for this run (e.g. `album-record-store`, `artist-caption-bedroom`).", "example": "artist-caption-bedroom" }, "lipsync": { "type": "boolean", "description": "Whether lipsync was applied (audio-driven mouth animation)." }, "audio": { "type": "object", "description": "Metadata about the song clip used in the final composition.", "properties": { "songTitle": { "type": "string" }, "songFilename": { "type": "string" }, "startSeconds": { "type": "number", "description": "Offset into the source song where the clip starts." }, "durationSeconds": { "type": "number", "description": "Length of the audio clip in seconds." }, "clipLyrics": { "type": "string", "nullable": true, "description": "Lyrics for the chosen clip, when available." }, "clipMood": { "type": "string", "nullable": true, "description": "Inferred mood for the chosen clip, when available." } } } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Create Task Source: https://recoupable.dev/docs/api-reference/tasks/create Create a new scheduled task that runs a prompt against an artist on a recurring cron schedule. The response matches the [GET endpoint](/docs/api-reference/tasks/get) (a `TasksResponse` with the created task in the `tasks` array). ## POST /api/tasks Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/tasks": { "post": { "description": "Create a new scheduled task that runs a prompt against an artist on a recurring cron schedule. The response matches the [GET endpoint](/docs/api-reference/tasks/get) (a `TasksResponse` with the created task in the `tasks` array).", "requestBody": { "description": "Task to create", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateTaskRequest" } } } }, "responses": { "200": { "description": "Task created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TasksResponse" } } } }, "400": { "description": "Bad request - missing required fields or invalid body", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Payment required - the account's plan does not allow this task. `limit` is `task_count` when the account already has `task_limit` enabled tasks, or `min_cadence` when the cron schedule fires more often than `min_cadence_minutes` allows. Nothing is written; upgrade at `billingUrl`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlanLimitError" }, "examples": { "taskCount": { "summary": "Free account creating a second task", "value": { "status": "error", "error": "plan_limit", "limit": "task_count", "message": "Free includes 1 task. Starter includes 3, Pro is unlimited.", "plan": "free", "task_limit": 1, "min_cadence_minutes": 10080, "current_task_count": 1, "billingUrl": "https://app.recoupable.dev/plan" } }, "minCadence": { "summary": "Free account scheduling a daily task", "value": { "status": "error", "error": "plan_limit", "limit": "min_cadence", "message": "Free runs tasks weekly at most. Starter runs daily, Pro runs hourly.", "plan": "free", "task_limit": 1, "min_cadence_minutes": 10080, "current_task_count": 0, "billingUrl": "https://app.recoupable.dev/plan" } } } } } }, "403": { "description": "Forbidden - account_id is outside caller authorization scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "CreateTaskRequest": { "type": "object", "required": [ "title", "prompt", "schedule", "artist_account_id" ], "properties": { "title": { "type": "string", "description": "Descriptive title of the task", "example": "Weekly Genre Pulse Check" }, "prompt": { "type": "string", "description": "Instruction/prompt executed by the task", "example": "Execute this weekly genre analysis workflow and email a summary to the team." }, "schedule": { "type": "string", "description": "Cron expression defining when the task runs (e.g., '0 9 * * 4' for Thursdays at 9 AM)", "example": "0 9 * * 4" }, "timezone": { "type": "string", "default": "UTC", "description": "Optional IANA time zone (e.g. America/New_York) the cron schedule is interpreted in, DST-aware. Defaults to UTC. Applied to the Trigger.dev schedule (the source of truth); not stored separately.", "example": "America/New_York" }, "account_id": { "type": "string", "format": "uuid", "description": "UUID of the account to create the task for. Only applicable when the authenticated account has access to multiple accounts via organization membership. If not provided, the task is created for the API key's own account.", "example": "848cd58d-700f-4b38-ab4c-d9f52a1b2c3d" }, "artist_account_id": { "type": "string", "format": "uuid", "description": "UUID of the associated artist account", "example": "1873859c-dd37-4e9a-9bac-80d35a1b2c3d" }, "model": { "type": "string", "description": "Optional AI model ID. Omit it to use the configured default. Empty strings are accepted by input validation; select a supported model when explicitly overriding." } } }, "TasksResponse": { "type": "object", "required": [ "status", "tasks" ], "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "tasks": { "type": "array", "items": { "$ref": "#/components/schemas/Task" }, "description": "Array of task objects" }, "error": { "type": "string", "description": "Error message (only present if status is error)" } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } }, "PlanLimitError": { "type": "object", "required": [ "status", "error", "limit", "message", "plan", "task_limit", "min_cadence_minutes", "current_task_count", "billingUrl" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `error` for this response shape" }, "error": { "type": "string", "enum": [ "plan_limit" ], "description": "Machine-readable code. Literally `plan_limit`." }, "limit": { "type": "string", "enum": [ "task_count", "min_cadence" ], "description": "Which entitlement blocked the request: `task_count` (the plan's enabled-task cap is reached) or `min_cadence` (the cron fires more often than the plan allows)." }, "message": { "type": "string", "description": "Human-readable explanation naming the plan and the limit, suitable to show to a user.", "example": "Free includes 1 task. Starter includes 3, Pro is unlimited." }, "plan": { "type": "string", "enum": [ "free", "starter", "pro" ], "description": "The account's current plan, resolved from its subscription." }, "task_limit": { "type": "integer", "nullable": true, "minimum": 1, "description": "Maximum enabled tasks on the current plan. Null means uncapped.", "example": 1 }, "min_cadence_minutes": { "type": "integer", "minimum": 1, "description": "Shortest allowed gap between two consecutive runs of a task on the current plan, in minutes (Free 10080, Starter 1440, Pro 60).", "example": 10080 }, "current_task_count": { "type": "integer", "minimum": 0, "description": "Enabled tasks the account has right now, excluding the one being created or updated.", "example": 1 }, "billingUrl": { "type": "string", "format": "uri", "description": "Static link to `/plan` in the Recoup app, where a human can upgrade. Constant; retrying creates nothing.", "example": "https://app.recoupable.dev/plan" } } }, "Task": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the task" }, "title": { "type": "string", "description": "Descriptive title or name of the task" }, "prompt": { "type": "string", "description": "Detailed instruction or prompt for task execution" }, "schedule": { "type": "string", "description": "Cron expression defining when the task should execute (e.g., '0 10 * * *')" }, "timezone": { "type": "string", "nullable": true, "description": "IANA time zone the task's cron is interpreted in, read from the Trigger.dev schedule (the source of truth). Null when the task has no schedule or the timezone can't be read.", "example": "America/New_York" }, "account_id": { "type": "string", "format": "uuid", "description": "Unique identifier for the associated account" }, "artist_account_id": { "type": "string", "format": "uuid", "description": "Unique identifier for the associated artist account" }, "enabled": { "type": "boolean", "nullable": true, "description": "Whether the task is enabled. Defaults to true." }, "trigger_schedule_id": { "type": "string", "nullable": true, "description": "Identifier for the trigger schedule associated with this task" }, "recent_runs": { "type": "array", "items": { "$ref": "#/components/schemas/TaskRunResponse" }, "description": "Last 5 runs for this task, sourced from the Trigger.dev API." }, "upcoming": { "type": "array", "items": { "type": "string", "format": "date-time" }, "description": "Next scheduled run times." }, "owner_email": { "type": "string", "nullable": true, "description": "Primary email address for the task owner account when one exists." }, "artist_name": { "type": "string", "nullable": true, "description": "Display name of the artist account the task runs for (`artist_account_id`), so a task list can label rows without a roster lookup. Null when the artist account no longer exists.", "example": "Braden Bales" }, "model": { "type": "string", "nullable": true, "minLength": 1, "description": "AI model identifier used when the task runs. When present as a string, must be non-empty; otherwise `null` if unset." } } }, "TaskRunResponse": { "type": "object", "description": "Raw Trigger.dev SDK run object. The API passes through the SDK response without field mapping. See https://trigger.dev/docs/management/runs/retrieve for the full reference. When listing runs, `output`, `error`, `payload`, and `attempts` are not included.", "required": [ "id", "status", "taskIdentifier", "createdAt", "updatedAt" ], "properties": { "id": { "type": "string", "description": "The unique run identifier, prefixed with `run_`" }, "status": { "type": "string", "enum": [ "PENDING_VERSION", "DELAYED", "QUEUED", "EXECUTING", "REATTEMPTING", "FROZEN", "COMPLETED", "CANCELED", "FAILED", "CRASHED", "INTERRUPTED", "SYSTEM_FAILURE" ], "description": "Current run status" }, "taskIdentifier": { "type": "string", "description": "The task type identifier (e.g. 'setup-sandbox', 'run-sandbox-command')" }, "idempotencyKey": { "type": "string", "nullable": true, "description": "Idempotency key used to deduplicate trigger requests" }, "version": { "type": "string", "description": "The worker version that executed the run" }, "isTest": { "type": "boolean", "description": "Whether this is a test run" }, "createdAt": { "type": "string", "format": "date-time", "description": "When the run was created (ISO 8601)" }, "updatedAt": { "type": "string", "format": "date-time", "description": "When the run was last updated (ISO 8601)" }, "startedAt": { "type": "string", "format": "date-time", "nullable": true, "description": "When execution started (null if not yet started)" }, "finishedAt": { "type": "string", "format": "date-time", "nullable": true, "description": "When the run finished (null if still running)" }, "delayedUntil": { "type": "string", "format": "date-time", "nullable": true, "description": "If delayed, when the run becomes eligible to execute" }, "ttl": { "description": "Time-to-live. If the run is not started within this duration, it expires.", "nullable": true }, "expiredAt": { "type": "string", "format": "date-time", "nullable": true, "description": "When the run expired (null if not expired)" }, "tags": { "type": "array", "items": { "type": "string" }, "description": "Tags associated with this run (max 10)" }, "metadata": { "type": "object", "nullable": true, "description": "JSON metadata attached to the run" }, "costInCents": { "type": "number", "description": "Compute cost of the run in cents" }, "baseCostInCents": { "type": "number", "description": "Base invocation cost in cents" }, "durationMs": { "type": "number", "description": "Compute duration in milliseconds" }, "env": { "type": "object", "description": "Environment the run executed in", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "user": { "type": "string", "nullable": true } } }, "depth": { "type": "integer", "description": "Nesting depth for child runs" }, "batchId": { "type": "string", "nullable": true, "description": "Batch ID if triggered as part of a batch" }, "triggerFunction": { "type": "string", "enum": [ "trigger", "triggerAndWait", "batchTrigger", "batchTriggerAndWait" ], "description": "The function used to trigger this run" }, "payload": { "description": "Input payload for the task. Only present when retrieving by runId.", "nullable": true }, "output": { "description": "Task output data. Only present when retrieving by runId, and only populated when `status` is `COMPLETED`. Shape varies by `taskIdentifier`. The `create-content` task returns the schema below; other tasks return their own.", "nullable": true, "oneOf": [ { "$ref": "#/components/schemas/CreateContentRunOutput" }, { "type": "object", "description": "Generic task output (for tasks not yet schema'd)" } ] }, "error": { "type": "object", "nullable": true, "description": "Error details if the run failed. Only present when retrieving by runId.", "properties": { "message": { "type": "string", "description": "Human-readable error message" }, "name": { "type": "string", "description": "Error name or type" }, "stackTrace": { "type": "string", "description": "Stack trace" } } }, "attempts": { "type": "array", "description": "Attempt history. Only present when retrieving by runId.", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Attempt ID, prefixed with `attempt_`" }, "status": { "type": "string", "enum": [ "PENDING", "EXECUTING", "PAUSED", "COMPLETED", "FAILED", "CANCELED" ] }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "startedAt": { "type": "string", "format": "date-time", "nullable": true }, "completedAt": { "type": "string", "format": "date-time", "nullable": true }, "error": { "type": "object", "nullable": true, "properties": { "message": { "type": "string" }, "name": { "type": "string" }, "stackTrace": { "type": "string" } } } } } }, "schedule": { "type": "object", "nullable": true, "description": "Schedule information if triggered by a schedule. Only present when retrieving by runId." }, "relatedRuns": { "type": "object", "nullable": true, "description": "Related run references (root, parent, children). Only present when retrieving by runId." } } }, "CreateContentRunOutput": { "type": "object", "description": "Output payload for a `create-content` task run. Returned in `output` when `status` is `COMPLETED`. Agents should poll `/api/tasks/runs?runId=…` and read these fields once the run is done.", "required": [ "videoSourceUrl", "imageUrl", "captionText", "template" ], "properties": { "videoSourceUrl": { "type": "string", "format": "uri", "description": "URL to the rendered final 9:16 video (image + motion + audio + caption already composed)." }, "imageUrl": { "type": "string", "format": "uri", "description": "URL to the base image used for video generation." }, "captionText": { "type": "string", "description": "The caption text burned into the video." }, "template": { "type": "string", "description": "Template id used for this run (e.g. `album-record-store`, `artist-caption-bedroom`).", "example": "artist-caption-bedroom" }, "lipsync": { "type": "boolean", "description": "Whether lipsync was applied (audio-driven mouth animation)." }, "audio": { "type": "object", "description": "Metadata about the song clip used in the final composition.", "properties": { "songTitle": { "type": "string" }, "songFilename": { "type": "string" }, "startSeconds": { "type": "number", "description": "Offset into the source song where the clip starts." }, "durationSeconds": { "type": "number", "description": "Length of the audio clip in seconds." }, "clipLyrics": { "type": "string", "nullable": true, "description": "Lyrics for the chosen clip, when available." }, "clipMood": { "type": "string", "nullable": true, "description": "Inferred mood for the chosen clip, when available." } } } } } } } } ``` # Update Task Source: https://recoupable.dev/docs/api-reference/tasks/update Update scheduled task ## PATCH /api/tasks Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/tasks": { "patch": { "summary": "Update scheduled task", "description": "Update an existing scheduled task. Only the id field is required; any additional fields you include will be updated on the task. The response shape matches the GET endpoint (an array containing the updated task).", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "JSON object with `id` and optional fields to merge onto the task.", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateTaskRequest" }, "examples": { "updateTitleAndSchedule": { "summary": "Change title and cron", "value": { "id": "aade2bce-55c7-468e-a606-c4e76fb2ea2a", "title": "Weekly Genre Pulse Check (rev)", "schedule": "0 10 * * 4" } }, "toggleEnabled": { "summary": "Pause or resume a task", "value": { "id": "aade2bce-55c7-468e-a606-c4e76fb2ea2a", "enabled": false } }, "changeModel": { "summary": "Switch model only", "value": { "id": "aade2bce-55c7-468e-a606-c4e76fb2ea2a", "model": "claude-sonnet-4-20250514" } } } } } }, "responses": { "200": { "description": "Task updated successfully. Body is a `TasksResponse` with the updated task in `tasks` (typically one task).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TasksResponse" } } } }, "400": { "description": "Bad request — missing `id`, empty strings where a field is provided, or other validation failure from the request body. The body reports the first Zod issue via `missing_fields` and `error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateTaskValidationErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Payment required - the account's plan does not allow this task. `limit` is `task_count` when the account already has `task_limit` enabled tasks, or `min_cadence` when the cron schedule fires more often than `min_cadence_minutes` allows. Nothing is written; upgrade at `billingUrl`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlanLimitError" }, "examples": { "taskCount": { "summary": "Free account re-enabling a disabled task while another is enabled", "value": { "status": "error", "error": "plan_limit", "limit": "task_count", "message": "Free includes 1 task. Starter includes 3, Pro is unlimited.", "plan": "free", "task_limit": 1, "min_cadence_minutes": 10080, "current_task_count": 1, "billingUrl": "https://app.recoupable.dev/plan" } }, "minCadence": { "summary": "Free account changing a task to a daily schedule", "value": { "status": "error", "error": "plan_limit", "limit": "min_cadence", "message": "Free runs tasks weekly at most. Starter runs daily, Pro runs hourly.", "plan": "free", "task_limit": 1, "min_cadence_minutes": 10080, "current_task_count": 0, "billingUrl": "https://app.recoupable.dev/plan" } } } } } }, "403": { "description": "Forbidden - account_id is outside caller authorization scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No task exists with the given `id`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateTaskFailureResponse" }, "examples": { "notFound": { "summary": "Unknown task id", "value": { "status": "error", "error": "Task not found" } } } } } }, "500": { "description": "Internal server error while updating the task or syncing the Trigger.dev schedule.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateTaskFailureResponse" } } } } } } } }, "components": { "schemas": { "UpdateTaskRequest": { "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "UUID of the task to update", "example": "aade2bce-55c7-468e-a606-c4e76fb2ea2a" }, "title": { "type": "string", "minLength": 1, "description": "New descriptive title. If sent, must be a non-empty string.", "example": "Weekly Genre Pulse Check (Updated)" }, "prompt": { "type": "string", "minLength": 1, "description": "New instruction/prompt executed by the task. If sent, must be a non-empty string.", "example": "Execute this weekly genre analysis workflow and email a summary to the team." }, "schedule": { "type": "string", "minLength": 1, "description": "New cron expression. If sent, must be non-empty and valid for your environment.", "example": "0 10 * * 4" }, "timezone": { "type": "string", "description": "Optional IANA time zone the cron is interpreted in (DST-aware). If sent, updates the Trigger.dev schedule; a timezone-only change re-syncs the schedule. A cron-only update preserves the existing time zone.", "example": "America/New_York" }, "account_id": { "type": "string", "format": "uuid", "description": "UUID of the account to update the task for. Only applicable when the authenticated account has access to multiple accounts via organization membership. If not provided, the task is updated for the API key's own account.", "example": "848cd58d-700f-4b38-ab4c-d9f52a1b2c3d" }, "artist_account_id": { "type": "string", "format": "uuid", "description": "UUID of the artist account to associate with the task after update", "example": "1873859c-dd37-4e9a-9bac-80d35a1b2c3d" }, "enabled": { "type": "boolean", "nullable": true, "description": "Whether the task is enabled (`true` / `false`), or `null` to clear an explicit override depending on server rules.", "example": true }, "model": { "type": "string", "minLength": 1, "description": "AI model identifier to use when the task runs. If sent, must be a non-empty string.", "example": "claude-sonnet-4-20250514" } } }, "TasksResponse": { "type": "object", "required": [ "status", "tasks" ], "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "tasks": { "type": "array", "items": { "$ref": "#/components/schemas/Task" }, "description": "Array of task objects" }, "error": { "type": "string", "description": "Error message (only present if status is error)" } } }, "UpdateTaskValidationErrorResponse": { "type": "object", "required": [ "status", "missing_fields", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `error` when validation fails" }, "missing_fields": { "type": "array", "description": "JSON path segments to the first field that failed validation (from Zod), e.g. `[\"id\"]` or `[\"schedule\"]`", "items": { "oneOf": [ { "type": "string" }, { "type": "integer" } ] } }, "error": { "type": "string", "description": "Validation message for the first failing field" } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } }, "PlanLimitError": { "type": "object", "required": [ "status", "error", "limit", "message", "plan", "task_limit", "min_cadence_minutes", "current_task_count", "billingUrl" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `error` for this response shape" }, "error": { "type": "string", "enum": [ "plan_limit" ], "description": "Machine-readable code. Literally `plan_limit`." }, "limit": { "type": "string", "enum": [ "task_count", "min_cadence" ], "description": "Which entitlement blocked the request: `task_count` (the plan's enabled-task cap is reached) or `min_cadence` (the cron fires more often than the plan allows)." }, "message": { "type": "string", "description": "Human-readable explanation naming the plan and the limit, suitable to show to a user.", "example": "Free includes 1 task. Starter includes 3, Pro is unlimited." }, "plan": { "type": "string", "enum": [ "free", "starter", "pro" ], "description": "The account's current plan, resolved from its subscription." }, "task_limit": { "type": "integer", "nullable": true, "minimum": 1, "description": "Maximum enabled tasks on the current plan. Null means uncapped.", "example": 1 }, "min_cadence_minutes": { "type": "integer", "minimum": 1, "description": "Shortest allowed gap between two consecutive runs of a task on the current plan, in minutes (Free 10080, Starter 1440, Pro 60).", "example": 10080 }, "current_task_count": { "type": "integer", "minimum": 0, "description": "Enabled tasks the account has right now, excluding the one being created or updated.", "example": 1 }, "billingUrl": { "type": "string", "format": "uri", "description": "Static link to `/plan` in the Recoup app, where a human can upgrade. Constant; retrying creates nothing.", "example": "https://app.recoupable.dev/plan" } } }, "UpdateTaskFailureResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `error` for this response shape" }, "error": { "type": "string", "description": "Human-readable error message (for example `Task not found` on 404)" } } }, "Task": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the task" }, "title": { "type": "string", "description": "Descriptive title or name of the task" }, "prompt": { "type": "string", "description": "Detailed instruction or prompt for task execution" }, "schedule": { "type": "string", "description": "Cron expression defining when the task should execute (e.g., '0 10 * * *')" }, "timezone": { "type": "string", "nullable": true, "description": "IANA time zone the task's cron is interpreted in, read from the Trigger.dev schedule (the source of truth). Null when the task has no schedule or the timezone can't be read.", "example": "America/New_York" }, "account_id": { "type": "string", "format": "uuid", "description": "Unique identifier for the associated account" }, "artist_account_id": { "type": "string", "format": "uuid", "description": "Unique identifier for the associated artist account" }, "enabled": { "type": "boolean", "nullable": true, "description": "Whether the task is enabled. Defaults to true." }, "trigger_schedule_id": { "type": "string", "nullable": true, "description": "Identifier for the trigger schedule associated with this task" }, "recent_runs": { "type": "array", "items": { "$ref": "#/components/schemas/TaskRunResponse" }, "description": "Last 5 runs for this task, sourced from the Trigger.dev API." }, "upcoming": { "type": "array", "items": { "type": "string", "format": "date-time" }, "description": "Next scheduled run times." }, "owner_email": { "type": "string", "nullable": true, "description": "Primary email address for the task owner account when one exists." }, "artist_name": { "type": "string", "nullable": true, "description": "Display name of the artist account the task runs for (`artist_account_id`), so a task list can label rows without a roster lookup. Null when the artist account no longer exists.", "example": "Braden Bales" }, "model": { "type": "string", "nullable": true, "minLength": 1, "description": "AI model identifier used when the task runs. When present as a string, must be non-empty; otherwise `null` if unset." } } }, "TaskRunResponse": { "type": "object", "description": "Raw Trigger.dev SDK run object. The API passes through the SDK response without field mapping. See https://trigger.dev/docs/management/runs/retrieve for the full reference. When listing runs, `output`, `error`, `payload`, and `attempts` are not included.", "required": [ "id", "status", "taskIdentifier", "createdAt", "updatedAt" ], "properties": { "id": { "type": "string", "description": "The unique run identifier, prefixed with `run_`" }, "status": { "type": "string", "enum": [ "PENDING_VERSION", "DELAYED", "QUEUED", "EXECUTING", "REATTEMPTING", "FROZEN", "COMPLETED", "CANCELED", "FAILED", "CRASHED", "INTERRUPTED", "SYSTEM_FAILURE" ], "description": "Current run status" }, "taskIdentifier": { "type": "string", "description": "The task type identifier (e.g. 'setup-sandbox', 'run-sandbox-command')" }, "idempotencyKey": { "type": "string", "nullable": true, "description": "Idempotency key used to deduplicate trigger requests" }, "version": { "type": "string", "description": "The worker version that executed the run" }, "isTest": { "type": "boolean", "description": "Whether this is a test run" }, "createdAt": { "type": "string", "format": "date-time", "description": "When the run was created (ISO 8601)" }, "updatedAt": { "type": "string", "format": "date-time", "description": "When the run was last updated (ISO 8601)" }, "startedAt": { "type": "string", "format": "date-time", "nullable": true, "description": "When execution started (null if not yet started)" }, "finishedAt": { "type": "string", "format": "date-time", "nullable": true, "description": "When the run finished (null if still running)" }, "delayedUntil": { "type": "string", "format": "date-time", "nullable": true, "description": "If delayed, when the run becomes eligible to execute" }, "ttl": { "description": "Time-to-live. If the run is not started within this duration, it expires.", "nullable": true }, "expiredAt": { "type": "string", "format": "date-time", "nullable": true, "description": "When the run expired (null if not expired)" }, "tags": { "type": "array", "items": { "type": "string" }, "description": "Tags associated with this run (max 10)" }, "metadata": { "type": "object", "nullable": true, "description": "JSON metadata attached to the run" }, "costInCents": { "type": "number", "description": "Compute cost of the run in cents" }, "baseCostInCents": { "type": "number", "description": "Base invocation cost in cents" }, "durationMs": { "type": "number", "description": "Compute duration in milliseconds" }, "env": { "type": "object", "description": "Environment the run executed in", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "user": { "type": "string", "nullable": true } } }, "depth": { "type": "integer", "description": "Nesting depth for child runs" }, "batchId": { "type": "string", "nullable": true, "description": "Batch ID if triggered as part of a batch" }, "triggerFunction": { "type": "string", "enum": [ "trigger", "triggerAndWait", "batchTrigger", "batchTriggerAndWait" ], "description": "The function used to trigger this run" }, "payload": { "description": "Input payload for the task. Only present when retrieving by runId.", "nullable": true }, "output": { "description": "Task output data. Only present when retrieving by runId, and only populated when `status` is `COMPLETED`. Shape varies by `taskIdentifier`. The `create-content` task returns the schema below; other tasks return their own.", "nullable": true, "oneOf": [ { "$ref": "#/components/schemas/CreateContentRunOutput" }, { "type": "object", "description": "Generic task output (for tasks not yet schema'd)" } ] }, "error": { "type": "object", "nullable": true, "description": "Error details if the run failed. Only present when retrieving by runId.", "properties": { "message": { "type": "string", "description": "Human-readable error message" }, "name": { "type": "string", "description": "Error name or type" }, "stackTrace": { "type": "string", "description": "Stack trace" } } }, "attempts": { "type": "array", "description": "Attempt history. Only present when retrieving by runId.", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Attempt ID, prefixed with `attempt_`" }, "status": { "type": "string", "enum": [ "PENDING", "EXECUTING", "PAUSED", "COMPLETED", "FAILED", "CANCELED" ] }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "startedAt": { "type": "string", "format": "date-time", "nullable": true }, "completedAt": { "type": "string", "format": "date-time", "nullable": true }, "error": { "type": "object", "nullable": true, "properties": { "message": { "type": "string" }, "name": { "type": "string" }, "stackTrace": { "type": "string" } } } } } }, "schedule": { "type": "object", "nullable": true, "description": "Schedule information if triggered by a schedule. Only present when retrieving by runId." }, "relatedRuns": { "type": "object", "nullable": true, "description": "Related run references (root, parent, children). Only present when retrieving by runId." } } }, "CreateContentRunOutput": { "type": "object", "description": "Output payload for a `create-content` task run. Returned in `output` when `status` is `COMPLETED`. Agents should poll `/api/tasks/runs?runId=…` and read these fields once the run is done.", "required": [ "videoSourceUrl", "imageUrl", "captionText", "template" ], "properties": { "videoSourceUrl": { "type": "string", "format": "uri", "description": "URL to the rendered final 9:16 video (image + motion + audio + caption already composed)." }, "imageUrl": { "type": "string", "format": "uri", "description": "URL to the base image used for video generation." }, "captionText": { "type": "string", "description": "The caption text burned into the video." }, "template": { "type": "string", "description": "Template id used for this run (e.g. `album-record-store`, `artist-caption-bedroom`).", "example": "artist-caption-bedroom" }, "lipsync": { "type": "boolean", "description": "Whether lipsync was applied (audio-driven mouth animation)." }, "audio": { "type": "object", "description": "Metadata about the song clip used in the final composition.", "properties": { "songTitle": { "type": "string" }, "songFilename": { "type": "string" }, "startSeconds": { "type": "number", "description": "Offset into the source song where the clip starts." }, "durationSeconds": { "type": "number", "description": "Length of the audio clip in seconds." }, "clipLyrics": { "type": "string", "nullable": true, "description": "Lyrics for the chosen clip, when available." }, "clipMood": { "type": "string", "nullable": true, "description": "Inferred mood for the chosen clip, when available." } } } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Delete Task Source: https://recoupable.dev/docs/api-reference/tasks/delete Delete scheduled task ## DELETE /api/tasks Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/tasks": { "delete": { "summary": "Delete scheduled task", "description": "Delete an existing scheduled task by `id`. Returns only the delete operation status.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "JSON object containing the task `id` to delete.", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteTaskRequest" }, "examples": { "deleteTask": { "summary": "Delete one task", "value": { "id": "aade2bce-55c7-468e-a606-c4e76fb2ea2a" } } } } } }, "responses": { "200": { "description": "Task deleted successfully.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteTaskResponse" } } } }, "400": { "description": "Bad request — missing `id` or invalid request body.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteTaskValidationErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden - account_id is outside caller authorization scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No task exists with the given `id`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteTaskFailureResponse" }, "examples": { "notFound": { "summary": "Unknown task id", "value": { "status": "error", "error": "Task not found" } } } } } }, "500": { "description": "Internal server error while deleting the task or deleting its Trigger.dev schedule.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteTaskFailureResponse" } } } } } } } }, "components": { "schemas": { "DeleteTaskRequest": { "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "UUID of the task to delete", "example": "aade2bce-55c7-468e-a606-c4e76fb2ea2a" } } }, "DeleteTaskResponse": { "type": "object", "required": [ "status" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the delete operation" } } }, "DeleteTaskValidationErrorResponse": { "type": "object", "required": [ "status", "missing_fields", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `error` when validation fails" }, "missing_fields": { "type": "array", "description": "JSON path segments to the first field that failed validation (from Zod), e.g. `[\"id\"]`", "items": { "oneOf": [ { "type": "string" }, { "type": "integer" } ] } }, "error": { "type": "string", "description": "Validation message for the first failing field" } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } }, "DeleteTaskFailureResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `error` for this response shape" }, "error": { "type": "string", "description": "Human-readable error message (for example `Task not found` on 404)" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Task Runs Source: https://recoupable.dev/docs/api-reference/tasks/runs Get task runs ## GET /api/tasks/runs Full OpenAPI specification: https://recoupable.dev/docs/spec/releases.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Releases", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/tasks/runs": { "get": { "summary": "Get task runs", "description": "Returns task runs for the authenticated account. When `runId` is provided, the response contains that single run (`runs` length 1) or 404 if not found. When `runId` is omitted, returns recent runs filtered by account context (default authenticated account, or `account_id` override when authorized).", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "runId", "in": "query", "description": "The unique identifier of a specific task run to retrieve. If omitted, returns a list of recent runs for the authenticated account.", "required": false, "schema": { "type": "string" } }, { "name": "limit", "in": "query", "description": "Maximum number of runs to return when listing (ignored when runId is provided). Default 20, max 100.", "required": false, "schema": { "type": "integer", "default": 20, "minimum": 1, "maximum": 100 } }, { "name": "account_id", "in": "query", "description": "Filter runs by account ID. When provided, returns runs tagged with account:. Only applicable when the authenticated account has access to multiple accounts via organization membership.", "required": false, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Task runs retrieved successfully.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TaskRunListResponse" } } } }, "400": { "description": "Bad request - invalid query parameters (for example non-numeric `limit`, out-of-range `limit`, or invalid empty values).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TaskRunsErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TaskRunsErrorResponse" } } } }, "403": { "description": "Forbidden - account_id is not accessible with the provided credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TaskRunsErrorResponse" } } } }, "404": { "description": "Task run not found (only when runId is provided)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TaskRunsErrorResponse" }, "examples": { "notFound": { "summary": "Unknown run id", "value": { "status": "error", "error": "Task run not found" } } } } } }, "500": { "description": "Internal server error while fetching runs from Trigger.dev.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TaskRunsErrorResponse" } } } } } } } }, "components": { "schemas": { "TaskRunListResponse": { "type": "object", "required": [ "status", "runs" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Indicates the request was successful" }, "runs": { "type": "array", "items": { "$ref": "#/components/schemas/TaskRunResponse" }, "description": "List of recent task runs for the authenticated account. Each item is a raw Trigger.dev SDK run object (same shape as TaskRunResponse, but without `output` and `error` fields)." } } }, "TaskRunsErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `error` for this response shape" }, "error": { "type": "string", "description": "Human-readable error message" } } }, "TaskRunResponse": { "type": "object", "description": "Raw Trigger.dev SDK run object. The API passes through the SDK response without field mapping. See https://trigger.dev/docs/management/runs/retrieve for the full reference. When listing runs, `output`, `error`, `payload`, and `attempts` are not included.", "required": [ "id", "status", "taskIdentifier", "createdAt", "updatedAt" ], "properties": { "id": { "type": "string", "description": "The unique run identifier, prefixed with `run_`" }, "status": { "type": "string", "enum": [ "PENDING_VERSION", "DELAYED", "QUEUED", "EXECUTING", "REATTEMPTING", "FROZEN", "COMPLETED", "CANCELED", "FAILED", "CRASHED", "INTERRUPTED", "SYSTEM_FAILURE" ], "description": "Current run status" }, "taskIdentifier": { "type": "string", "description": "The task type identifier (e.g. 'setup-sandbox', 'run-sandbox-command')" }, "idempotencyKey": { "type": "string", "nullable": true, "description": "Idempotency key used to deduplicate trigger requests" }, "version": { "type": "string", "description": "The worker version that executed the run" }, "isTest": { "type": "boolean", "description": "Whether this is a test run" }, "createdAt": { "type": "string", "format": "date-time", "description": "When the run was created (ISO 8601)" }, "updatedAt": { "type": "string", "format": "date-time", "description": "When the run was last updated (ISO 8601)" }, "startedAt": { "type": "string", "format": "date-time", "nullable": true, "description": "When execution started (null if not yet started)" }, "finishedAt": { "type": "string", "format": "date-time", "nullable": true, "description": "When the run finished (null if still running)" }, "delayedUntil": { "type": "string", "format": "date-time", "nullable": true, "description": "If delayed, when the run becomes eligible to execute" }, "ttl": { "description": "Time-to-live. If the run is not started within this duration, it expires.", "nullable": true }, "expiredAt": { "type": "string", "format": "date-time", "nullable": true, "description": "When the run expired (null if not expired)" }, "tags": { "type": "array", "items": { "type": "string" }, "description": "Tags associated with this run (max 10)" }, "metadata": { "type": "object", "nullable": true, "description": "JSON metadata attached to the run" }, "costInCents": { "type": "number", "description": "Compute cost of the run in cents" }, "baseCostInCents": { "type": "number", "description": "Base invocation cost in cents" }, "durationMs": { "type": "number", "description": "Compute duration in milliseconds" }, "env": { "type": "object", "description": "Environment the run executed in", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "user": { "type": "string", "nullable": true } } }, "depth": { "type": "integer", "description": "Nesting depth for child runs" }, "batchId": { "type": "string", "nullable": true, "description": "Batch ID if triggered as part of a batch" }, "triggerFunction": { "type": "string", "enum": [ "trigger", "triggerAndWait", "batchTrigger", "batchTriggerAndWait" ], "description": "The function used to trigger this run" }, "payload": { "description": "Input payload for the task. Only present when retrieving by runId.", "nullable": true }, "output": { "description": "Task output data. Only present when retrieving by runId, and only populated when `status` is `COMPLETED`. Shape varies by `taskIdentifier`. The `create-content` task returns the schema below; other tasks return their own.", "nullable": true, "oneOf": [ { "$ref": "#/components/schemas/CreateContentRunOutput" }, { "type": "object", "description": "Generic task output (for tasks not yet schema'd)" } ] }, "error": { "type": "object", "nullable": true, "description": "Error details if the run failed. Only present when retrieving by runId.", "properties": { "message": { "type": "string", "description": "Human-readable error message" }, "name": { "type": "string", "description": "Error name or type" }, "stackTrace": { "type": "string", "description": "Stack trace" } } }, "attempts": { "type": "array", "description": "Attempt history. Only present when retrieving by runId.", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Attempt ID, prefixed with `attempt_`" }, "status": { "type": "string", "enum": [ "PENDING", "EXECUTING", "PAUSED", "COMPLETED", "FAILED", "CANCELED" ] }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "startedAt": { "type": "string", "format": "date-time", "nullable": true }, "completedAt": { "type": "string", "format": "date-time", "nullable": true }, "error": { "type": "object", "nullable": true, "properties": { "message": { "type": "string" }, "name": { "type": "string" }, "stackTrace": { "type": "string" } } } } } }, "schedule": { "type": "object", "nullable": true, "description": "Schedule information if triggered by a schedule. Only present when retrieving by runId." }, "relatedRuns": { "type": "object", "nullable": true, "description": "Related run references (root, parent, children). Only present when retrieving by runId." } } }, "CreateContentRunOutput": { "type": "object", "description": "Output payload for a `create-content` task run. Returned in `output` when `status` is `COMPLETED`. Agents should poll `/api/tasks/runs?runId=…` and read these fields once the run is done.", "required": [ "videoSourceUrl", "imageUrl", "captionText", "template" ], "properties": { "videoSourceUrl": { "type": "string", "format": "uri", "description": "URL to the rendered final 9:16 video (image + motion + audio + caption already composed)." }, "imageUrl": { "type": "string", "format": "uri", "description": "URL to the base image used for video generation." }, "captionText": { "type": "string", "description": "The caption text burned into the video." }, "template": { "type": "string", "description": "Template id used for this run (e.g. `album-record-store`, `artist-caption-bedroom`).", "example": "artist-caption-bedroom" }, "lipsync": { "type": "boolean", "description": "Whether lipsync was applied (audio-driven mouth animation)." }, "audio": { "type": "object", "description": "Metadata about the song clip used in the final composition.", "properties": { "songTitle": { "type": "string" }, "songFilename": { "type": "string" }, "startSeconds": { "type": "number", "description": "Offset into the source song where the clip starts." }, "durationSeconds": { "type": "number", "description": "Length of the audio clip in seconds." }, "clipLyrics": { "type": "string", "nullable": true, "description": "Lyrics for the chosen clip, when available." }, "clipMood": { "type": "string", "nullable": true, "description": "Inferred mood for the chosen clip, when available." } } } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Slack Webhook Source: https://recoupable.dev/docs/api-reference/content-agent/webhook Webhook endpoint for the Recoup Content Agent Slack bot. Receives @mention events from Slack and triggers content generation for the mentioned artist. The bot parses the mention text for ` [template] [batch=N] [lipsync]`, validates the artist, calls POST /api/content/create, and starts a background polling task that reports results back to the Slack thread. ## POST /api/content-agent/{platform} Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/content-agent/{platform}": { "post": { "description": "Webhook endpoint for the Recoup Content Agent Slack bot. Receives @mention events from Slack and triggers content generation for the mentioned artist. The bot parses the mention text for ` [template] [batch=N] [lipsync]`, validates the artist, calls POST /api/content/create, and starts a background polling task that reports results back to the Slack thread.\n\nFor Slack, also handles `url_verification` challenges during app setup.", "parameters": [ { "name": "platform", "in": "path", "description": "Chat platform identifier. Currently supports `slack`.", "required": true, "schema": { "type": "string", "enum": [ "slack" ] } } ], "requestBody": { "description": "Slack Events API payload (app_mention event or url_verification challenge)", "required": true, "content": { "application/json": { "schema": { "type": "object", "description": "Slack Events API envelope — the shape depends on the event type" } } } }, "responses": { "200": { "description": "Event processed successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" } } } } } }, "404": { "description": "Unknown platform" } } } } } } ``` # Task Callback Source: https://recoupable.dev/docs/api-reference/content-agent/callback Internal worker callback for a configured content agent. Requires the shared callback secret in x-callback-secret. Public content creation uses POST /api/content/create. ## POST /api/content-agent/callback Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/content-agent/callback": { "post": { "description": "Internal worker callback for a configured content agent. Requires the shared callback secret in x-callback-secret. Public content creation uses POST /api/content/create.\n\nInternal callback endpoint for the `poll-content-run` Trigger.dev task. Receives content generation results and posts them back to the originating Slack thread. Authenticated via the `x-callback-secret` header.\n\nThis endpoint is not intended for external use — it is called automatically by the polling task when content runs complete, fail, or time out.", "requestBody": { "description": "Content generation results from the polling task", "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "threadId", "status" ], "properties": { "threadId": { "type": "string", "description": "Chat SDK thread identifier for the originating Slack thread" }, "status": { "type": "string", "enum": [ "completed", "failed", "timeout" ], "description": "Overall status of the content generation batch" }, "results": { "type": "array", "description": "Per-run results", "items": { "type": "object", "required": [ "runId", "status" ], "properties": { "runId": { "type": "string", "description": "Trigger.dev run ID" }, "status": { "type": "string", "enum": [ "completed", "failed", "timeout" ] }, "videoUrl": { "type": "string", "description": "URL of the generated video (when completed)" }, "captionText": { "type": "string", "description": "Generated caption text (when completed)" }, "error": { "type": "string", "description": "Error message (when failed)" } } } }, "message": { "type": "string", "description": "Optional human-readable message" } } } } } }, "responses": { "200": { "description": "Callback processed and results posted to Slack", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "ok" } } } } } }, "400": { "description": "Invalid request body" }, "401": { "description": "Missing or invalid callback secret" }, "503": { "description": "Content agent is not configured.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string" } } } } } } }, "security": [ { "callbackSecret": [] } ] } } }, "components": { "securitySchemes": { "callbackSecret": { "type": "apiKey", "in": "header", "name": "x-callback-secret", "description": "Shared secret for the internal worker callback. Ordinary Recoup API keys and OAuth credentials are not accepted here." } } } } ``` # List Sandboxes Source: https://recoupable.dev/docs/api-reference/sandboxes/list List all sandboxes associated with the authenticated account and their current statuses. Returns sandbox details including lifecycle state, timeout remaining, and creation timestamp. Pass account_id to retrieve sandboxes for a specific account the API key has access to. Authentication is handled via the x-api-key header or Authorization Bearer token. ## GET /api/sandboxes Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/sandboxes": { "get": { "description": "List all sandboxes associated with the authenticated account and their current statuses. Returns sandbox details including lifecycle state, timeout remaining, and creation timestamp. Pass account_id to retrieve sandboxes for a specific account the API key has access to. Authentication is handled via the x-api-key header or Authorization Bearer token.", "parameters": [ { "name": "sandbox_id", "in": "query", "required": false, "description": "Filter by a specific sandbox ID. When provided, returns only the sandbox matching this ID. Must be a sandbox that your account or organization is an admin of.", "schema": { "type": "string", "example": "sbx_abc123def456" } }, { "name": "account_id", "in": "query", "description": "Filter to a specific account. Only applicable when the authenticated account has access to multiple accounts via organization membership.", "required": false, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Sandboxes retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxesResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } }, "403": { "description": "Forbidden - account_id is not a member of the organization or account tried to filter by an account_id they don't have access to", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } } } } } }, "components": { "schemas": { "SandboxesResponse": { "type": "object", "required": [ "status", "sandboxes" ], "description": "Response containing sandbox information", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "sandboxes": { "type": "array", "items": { "$ref": "#/components/schemas/Sandbox" }, "description": "Array of sandbox objects" }, "snapshot_id": { "type": "string", "description": "The account's saved snapshot ID used for creating new sandboxes. Null if no snapshot has been saved.", "example": "snap_abc123def456", "nullable": true }, "github_repo": { "type": "string", "description": "The GitHub repository URL associated with the account's sandbox environment. Used as the filesystem source when restoring sandboxes.", "example": "https://github.com/username/repo", "nullable": true }, "filetree": { "type": "array", "nullable": true, "description": "The recursive file tree of the account's GitHub repository. Null if no github_repo is set or if the fetch fails.", "items": { "$ref": "#/components/schemas/FileTreeEntry" } }, "error": { "type": "string", "description": "Error message (only present if status is error)" } } }, "SandboxErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Failed to create sandbox" } } }, "Sandbox": { "type": "object", "required": [ "sandboxId", "sandboxStatus", "timeout", "createdAt" ], "description": "A sandbox environment instance", "properties": { "sandboxId": { "type": "string", "description": "Unique identifier for the sandbox", "example": "sbx_abc123def456" }, "sandboxStatus": { "type": "string", "enum": [ "pending", "running", "stopping", "stopped", "failed" ], "description": "Current lifecycle state of the sandbox", "example": "running" }, "timeout": { "type": "integer", "description": "Milliseconds remaining before the sandbox stops automatically", "example": 300000 }, "createdAt": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp when the sandbox was created", "example": "2024-01-15T10:30:00.000Z" }, "runId": { "type": "string", "description": "Unique identifier for the command execution run. Only present if a command was provided when creating the sandbox. Use this with [GET /api/tasks/runs](/docs/api-reference/tasks/runs) to check the status and retrieve results.", "example": "run_xyz789abc123" } } }, "FileTreeEntry": { "type": "object", "required": [ "path", "type", "sha" ], "description": "A single entry in a GitHub repository file tree", "properties": { "path": { "type": "string", "description": "The file or directory path relative to the repository root", "example": "src/index.ts" }, "type": { "type": "string", "enum": [ "blob", "tree" ], "description": "The type of entry: blob for files, tree for directories" }, "sha": { "type": "string", "description": "The SHA hash of the entry", "example": "abc123def456" }, "size": { "type": "integer", "description": "The size of the file in bytes. Only present for blob entries.", "example": 1024 } } } } } } ``` # Create Sandbox Source: https://recoupable.dev/docs/api-reference/sandboxes/create Create a new ephemeral sandbox environment. Optionally executes a command or an OpenCode prompt if provided. Sandboxes are isolated Linux microVMs that can be used to evaluate account-generated code, run AI agent output safely, or execute reproducible tasks. The sandbox will automatically stop after the timeout period. If no command or prompt is provided, the sandbox is created without triggering any background task. Use the prompt parameter as a shortcut to run `opencode run ""` in the sandbox. Pass account_id to create a sandbox for a specific account the API key has access to. Authentication is handled via the x-api-key header or Authorization Bearer token. ## POST /api/sandboxes Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/sandboxes": { "post": { "description": "Create a new ephemeral sandbox environment. Optionally executes a command or an OpenCode prompt if provided. Sandboxes are isolated Linux microVMs that can be used to evaluate account-generated code, run AI agent output safely, or execute reproducible tasks. The sandbox will automatically stop after the timeout period. If no command or prompt is provided, the sandbox is created without triggering any background task. Use the prompt parameter as a shortcut to run `opencode run \"\"` in the sandbox. Pass account_id to create a sandbox for a specific account the API key has access to. Authentication is handled via the x-api-key header or Authorization Bearer token.", "requestBody": { "description": "Optional command execution parameters. If command and prompt are both omitted, sandbox is created without running any command. Use prompt as a shortcut for running OpenCode.", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSandboxRequest" } } } }, "responses": { "200": { "description": "Sandbox created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxesResponse" } } } }, "400": { "description": "Bad request - failed to create sandbox", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } }, "403": { "description": "Forbidden - account_id is not a member of the organization or account tried to use an account_id they don't have access to", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } } } } } }, "components": { "schemas": { "CreateSandboxRequest": { "type": "object", "description": "Request body for creating a new sandbox. All fields are optional - if no command or prompt is provided, sandbox is created without triggering a command execution task. Use prompt as a shortcut to run OpenCode with a given prompt instead of specifying command/args manually.", "properties": { "command": { "type": "string", "minLength": 1, "description": "The command to execute in the sandbox environment. If omitted, the sandbox is created without running any command.", "example": "ls" }, "args": { "type": "array", "items": { "type": "string" }, "description": "Optional arguments to pass to the command.", "example": [ "-la", "/home" ] }, "cwd": { "type": "string", "description": "Optional working directory for command execution.", "example": "/home/user" }, "prompt": { "type": "string", "minLength": 1, "description": "A prompt to pass to OpenCode in the sandbox. When provided, the sandbox will execute `opencode run \"\"`. Cannot be used together with command.", "example": "create a hello world index.html" }, "account_id": { "type": "string", "format": "uuid", "description": "UUID of the account to create the sandbox for. Only applicable when the authenticated account has access to multiple accounts via organization membership. If not provided, creates the sandbox for the API key's own account." } } }, "SandboxesResponse": { "type": "object", "required": [ "status", "sandboxes" ], "description": "Response containing sandbox information", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "sandboxes": { "type": "array", "items": { "$ref": "#/components/schemas/Sandbox" }, "description": "Array of sandbox objects" }, "snapshot_id": { "type": "string", "description": "The account's saved snapshot ID used for creating new sandboxes. Null if no snapshot has been saved.", "example": "snap_abc123def456", "nullable": true }, "github_repo": { "type": "string", "description": "The GitHub repository URL associated with the account's sandbox environment. Used as the filesystem source when restoring sandboxes.", "example": "https://github.com/username/repo", "nullable": true }, "filetree": { "type": "array", "nullable": true, "description": "The recursive file tree of the account's GitHub repository. Null if no github_repo is set or if the fetch fails.", "items": { "$ref": "#/components/schemas/FileTreeEntry" } }, "error": { "type": "string", "description": "Error message (only present if status is error)" } } }, "SandboxErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Failed to create sandbox" } } }, "Sandbox": { "type": "object", "required": [ "sandboxId", "sandboxStatus", "timeout", "createdAt" ], "description": "A sandbox environment instance", "properties": { "sandboxId": { "type": "string", "description": "Unique identifier for the sandbox", "example": "sbx_abc123def456" }, "sandboxStatus": { "type": "string", "enum": [ "pending", "running", "stopping", "stopped", "failed" ], "description": "Current lifecycle state of the sandbox", "example": "running" }, "timeout": { "type": "integer", "description": "Milliseconds remaining before the sandbox stops automatically", "example": 300000 }, "createdAt": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp when the sandbox was created", "example": "2024-01-15T10:30:00.000Z" }, "runId": { "type": "string", "description": "Unique identifier for the command execution run. Only present if a command was provided when creating the sandbox. Use this with [GET /api/tasks/runs](/docs/api-reference/tasks/runs) to check the status and retrieve results.", "example": "run_xyz789abc123" } } }, "FileTreeEntry": { "type": "object", "required": [ "path", "type", "sha" ], "description": "A single entry in a GitHub repository file tree", "properties": { "path": { "type": "string", "description": "The file or directory path relative to the repository root", "example": "src/index.ts" }, "type": { "type": "string", "enum": [ "blob", "tree" ], "description": "The type of entry: blob for files, tree for directories" }, "sha": { "type": "string", "description": "The SHA hash of the entry", "example": "abc123def456" }, "size": { "type": "integer", "description": "The size of the file in bytes. Only present for blob entries.", "example": 1024 } } } } } } ``` # Update Snapshot Source: https://recoupable.dev/docs/api-reference/sandboxes/snapshot Set a custom snapshot ID for an account. By default, updates the key owner's account. Pass account_id to target a specific account the API key has access to. This allows accounts to use a specific sandbox snapshot when creating new sandboxes, enabling reproducible environments with pre-configured tools, dependencies, and files. ## PATCH /api/sandboxes Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/sandboxes": { "patch": { "description": "Set a custom snapshot ID for an account. By default, updates the key owner's account. Pass account_id to target a specific account the API key has access to. This allows accounts to use a specific sandbox snapshot when creating new sandboxes, enabling reproducible environments with pre-configured tools, dependencies, and files.", "requestBody": { "description": "Snapshot configuration parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateSnapshotRequest" } } } }, "responses": { "200": { "description": "Snapshot ID updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateSnapshotResponse" } } } }, "400": { "description": "Bad request - invalid snapshot ID", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } }, "403": { "description": "Forbidden - account_id is not a member of the organization or account tried to use an account_id they don't have access to", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } } } } } }, "components": { "schemas": { "UpdateSnapshotRequest": { "type": "object", "required": [], "properties": { "snapshotId": { "type": "string", "description": "The snapshot ID to set for the account. This snapshot will be used as the base environment when creating new sandboxes.", "example": "snap_abc123def456" }, "github_repo": { "type": "string", "format": "uri", "description": "The GitHub repository URL to associate with the account's sandbox environment. Must be a valid URL.", "example": "https://github.com/org/repo" }, "account_id": { "type": "string", "format": "uuid", "description": "UUID of the account to update the snapshot for. Only applicable when the authenticated account has access to multiple accounts via organization membership. If not provided, updates the snapshot for the API key's own account." } } }, "UpdateSnapshotResponse": { "type": "object", "required": [ "account_id", "snapshot_id", "expires_at", "github_repo", "created_at" ], "properties": { "account_id": { "type": "string", "format": "uuid", "description": "The account ID this snapshot belongs to", "example": "550e8400-e29b-41d4-a716-446655440000" }, "snapshot_id": { "type": "string", "description": "The snapshot ID that was set for the account", "example": "snap_abc123def456" }, "expires_at": { "type": "string", "format": "date-time", "description": "When the snapshot expires", "example": "2027-01-01T00:00:00.000Z" }, "github_repo": { "type": "string", "nullable": true, "description": "The GitHub repository URL associated with the sandbox", "example": "https://github.com/org/repo" }, "created_at": { "type": "string", "format": "date-time", "nullable": true, "description": "When the snapshot record was created", "example": "2025-01-01T00:00:00.000Z" } } }, "SandboxErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Failed to create sandbox" } } } } } } ``` # Delete Sandbox Source: https://recoupable.dev/docs/api-reference/sandboxes/delete Delete a sandbox environment for the authenticated account. This permanently deletes the associated GitHub repository and removes the account's snapshot record from the database. By default, deletes the sandbox for the key owner's account. Pass account_id to target a specific account the API key has access to. Authentication is handled via the x-api-key header or Authorization Bearer token. ## DELETE /api/sandboxes Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/sandboxes": { "delete": { "description": "Delete a sandbox environment for the authenticated account. This permanently deletes the associated GitHub repository and removes the account's snapshot record from the database. By default, deletes the sandbox for the key owner's account. Pass account_id to target a specific account the API key has access to. Authentication is handled via the x-api-key header or Authorization Bearer token.", "requestBody": { "description": "Optional account targeting parameters", "required": false, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteSandboxRequest" } } } }, "responses": { "200": { "description": "Sandbox deleted successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteSandboxResponse" } } } }, "400": { "description": "Bad request - failed to delete sandbox", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } }, "403": { "description": "Forbidden - account_id is not a member of the organization or account tried to use an account_id they don't have access to", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } } } } } }, "components": { "schemas": { "DeleteSandboxRequest": { "type": "object", "properties": { "account_id": { "type": "string", "format": "uuid", "description": "UUID of the account to delete the sandbox for. Only applicable when the authenticated account has access to multiple accounts via organization membership. If not provided, deletes the sandbox for the API key's own account." } } }, "DeleteSandboxResponse": { "type": "object", "required": [ "status" ], "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the delete operation" }, "deleted_snapshot": { "nullable": true, "description": "The snapshot record that was deleted. Null if no snapshot existed for the account.", "$ref": "#/components/schemas/UpdateSnapshotResponse" }, "error": { "type": "string", "description": "Error message (only present if status is error)" } } }, "SandboxErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Failed to create sandbox" } } }, "UpdateSnapshotResponse": { "type": "object", "required": [ "account_id", "snapshot_id", "expires_at", "github_repo", "created_at" ], "properties": { "account_id": { "type": "string", "format": "uuid", "description": "The account ID this snapshot belongs to", "example": "550e8400-e29b-41d4-a716-446655440000" }, "snapshot_id": { "type": "string", "description": "The snapshot ID that was set for the account", "example": "snap_abc123def456" }, "expires_at": { "type": "string", "format": "date-time", "description": "When the snapshot expires", "example": "2027-01-01T00:00:00.000Z" }, "github_repo": { "type": "string", "nullable": true, "description": "The GitHub repository URL associated with the sandbox", "example": "https://github.com/org/repo" }, "created_at": { "type": "string", "format": "date-time", "nullable": true, "description": "When the snapshot record was created", "example": "2025-01-01T00:00:00.000Z" } } } } } } ``` # Setup Sandbox Source: https://recoupable.dev/docs/api-reference/sandboxes/setup Triggers the setup-sandbox background task to create a personal sandbox, provision a GitHub repo, take a snapshot, and shut down. By default, sets up the sandbox for the key owner's account. Pass account_id to target a specific account the API key has access to. Authentication is handled via the x-api-key header or Authorization Bearer token. ## POST /api/sandboxes/setup Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/sandboxes/setup": { "post": { "description": "Triggers the setup-sandbox background task to create a personal sandbox, provision a GitHub repo, take a snapshot, and shut down. By default, sets up the sandbox for the key owner's account. Pass account_id to target a specific account the API key has access to. Authentication is handled via the x-api-key header or Authorization Bearer token.", "requestBody": { "description": "Optional account targeting parameters", "required": false, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SetupSandboxRequest" } } } }, "responses": { "200": { "description": "Setup task triggered successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SetupSandboxResponse" } } } }, "400": { "description": "Bad request - invalid account_id format", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } } } } } }, "components": { "schemas": { "SetupSandboxRequest": { "type": "object", "properties": { "account_id": { "type": "string", "format": "uuid", "description": "UUID of the account to set up the sandbox for. Only applicable when the authenticated account has access to multiple accounts via organization membership. If not provided, sets up the sandbox for the API key's own account." } } }, "SetupSandboxResponse": { "type": "object", "required": [ "status", "runId" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the setup operation" }, "runId": { "type": "string", "description": "The Trigger.dev run ID for the setup-sandbox background task. Use this with [GET /api/tasks/runs](/docs/api-reference/tasks/runs) to check the status and retrieve results.", "example": "run_abc123def456" } } }, "SandboxErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Failed to create sandbox" } } } } } } ``` # Get File Contents Source: https://recoupable.dev/docs/api-reference/sandboxes/get-file Retrieve the contents of a file from the authenticated account's sandbox GitHub repository. Resolves the github_repo from the [account's snapshot](/docs/api-reference/sandboxes/list), then fetches the file at the specified path from the repository's main branch. **Text files are returned verbatim. Binary files (`.mp3`, `.png`, `.jpg`, `.mp4`, `.pdf`, etc.) are returned base64-encoded — decode before writing to disk.** Authentication is handled via the x-api-key header or Authorization Bearer token. ## GET /api/sandboxes/file Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/sandboxes/file": { "get": { "description": "Retrieve the contents of a file from the authenticated account's sandbox GitHub repository. Resolves the github_repo from the [account's snapshot](/docs/api-reference/sandboxes/list), then fetches the file at the specified path from the repository's main branch. **Text files are returned verbatim. Binary files (`.mp3`, `.png`, `.jpg`, `.mp4`, `.pdf`, etc.) are returned base64-encoded — decode before writing to disk.** Authentication is handled via the x-api-key header or Authorization Bearer token.", "parameters": [ { "name": "path", "in": "query", "required": true, "description": "The file path within the repository (e.g. \"src/index.ts\" or \"README.md\").", "schema": { "type": "string", "example": "src/index.ts" } } ], "responses": { "200": { "description": "File contents retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxFileResponse" } } } }, "400": { "description": "Bad request - missing or invalid path parameter", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } }, "403": { "description": "Forbidden - account does not have access", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } }, "404": { "description": "Not found - no snapshot, no github_repo, or file not found in repository", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } } } } } }, "components": { "schemas": { "SandboxFileResponse": { "type": "object", "required": [ "status", "content" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the operation" }, "content": { "type": "string", "description": "The file content. Text files (`.md`, `.json`, `.txt`, source code, etc.) are returned verbatim. Binary files (`.mp3`, `.png`, `.jpg`, `.mp4`, `.pdf`, etc.) are returned base64-encoded — decode before writing to disk. The encoding is inferred from the file extension server-side; `encoding` field is not yet returned but planned." } } }, "SandboxErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Failed to create sandbox" } } } } } } ``` # Stage File Source: https://recoupable.dev/docs/api-reference/sandboxes/stage-file Issue a presigned upload token so the browser can upload a file directly to Vercel Blob, then call `POST /api/sandboxes/files` with the resulting blob URL to commit it to the sandbox repo. Use `upload()` from `@vercel/blob/client` rather than calling this route by hand. Max file size 100MB. ## POST /api/sandboxes/staged-file Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/sandboxes/staged-file": { "post": { "description": "Issue a presigned upload token so the browser can upload a file directly to Vercel Blob, then call `POST /api/sandboxes/files` with the resulting blob URL to commit it to the sandbox repo. Use `upload()` from `@vercel/blob/client` rather than calling this route by hand. Max file size 100MB.", "requestBody": { "description": "`HandleUploadBody` envelope from `@vercel/blob/client`.", "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "responses": { "200": { "description": "Handshake response from `@vercel/blob/client` — return verbatim to the library.", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "401": { "description": "Missing or invalid auth on the handshake POST.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } }, "500": { "description": "Invalid body or upstream Vercel Blob failure.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } } } } } }, "components": { "schemas": { "SandboxErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Failed to create sandbox" } } } } } } ``` # Upload Files Source: https://recoupable.dev/docs/api-reference/sandboxes/upload-files Upload one or more files to the authenticated account's sandbox GitHub repository. Accepts an array of file URLs and commits each file to the specified directory path within the repository. Supports submodule resolution — if the target path falls within a git submodule, the file is committed to the submodule's repository. Authentication is handled via the x-api-key header or Authorization Bearer token. ## POST /api/sandboxes/files Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/sandboxes/files": { "post": { "description": "Upload one or more files to the authenticated account's sandbox GitHub repository. Accepts an array of file URLs and commits each file to the specified directory path within the repository. Supports submodule resolution — if the target path falls within a git submodule, the file is committed to the submodule's repository. Authentication is handled via the x-api-key header or Authorization Bearer token.", "requestBody": { "description": "JSON body containing file URLs and target path", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UploadSandboxFilesRequest" } } } }, "responses": { "200": { "description": "Files uploaded successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UploadSandboxFilesResponse" } } } }, "400": { "description": "Bad request - missing files or invalid path", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } }, "403": { "description": "Forbidden - account does not have access", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } }, "404": { "description": "Not found - no snapshot or no github_repo configured", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxErrorResponse" } } } } } } } }, "components": { "schemas": { "UploadSandboxFilesRequest": { "type": "object", "required": [ "files" ], "properties": { "path": { "type": "string", "description": "The target directory path within the repository to upload files to. Defaults to the repository root if omitted.", "example": "assets/images" }, "files": { "type": "array", "items": { "type": "object", "required": [ "url", "name" ], "properties": { "url": { "type": "string", "format": "uri", "description": "The URL of the file to upload", "example": "https://example.com/files/album-cover.png" }, "name": { "type": "string", "description": "The filename to use when committing to the repository", "example": "album-cover.png" } } }, "description": "Array of files to upload, each with a URL and target filename" }, "message": { "type": "string", "description": "Optional commit message. Defaults to 'Upload files via API'.", "example": "Add new album artwork" } } }, "UploadSandboxFilesResponse": { "type": "object", "required": [ "status", "uploaded" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the operation" }, "uploaded": { "type": "array", "items": { "type": "object", "properties": { "path": { "type": "string", "description": "The full path of the uploaded file in the repository" }, "sha": { "type": "string", "description": "The git SHA of the created/updated file" } } }, "description": "Array of uploaded file details" } } }, "SandboxErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Error message describing what went wrong", "example": "Failed to create sandbox" } } } } } } ``` # Create Session Source: https://recoupable.dev/docs/api-reference/sessions/create Create session ## POST /api/sessions Full OpenAPI specification: https://recoupable.dev/docs/spec/sessions.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Sessions", "description": "Sessions — sandboxed runs of an LLM-driven agent with tool use, lifecycle hooks, and persistence. Each session pairs one Vercel Sandbox with one or more chat threads.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "security": [ { "ApiKeyAuth": [] }, { "BearerAuth": [] } ], "paths": { "/api/sessions": { "post": { "summary": "Create session", "description": "Creates a new agent session and an initial empty chat in a single transaction. The session is created in the `provisioning` lifecycle state — a separate orchestration step claims it and starts the sandbox.", "requestBody": { "required": false, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSessionRequest" } } } }, "responses": { "200": { "description": "Session and initial chat created successfully.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSessionResponse" } } } }, "400": { "description": "Invalid request body — either malformed JSON or a field failed validation.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized — invalid or missing API key / Bearer token.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Server error — the session could not be persisted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "CreateSessionRequest": { "type": "object", "description": "All fields are optional. An empty body is valid and creates a personal session — the server provisions (or reuses) a workspace repo at `recoupable/` for the authenticated user. Include `organizationId` to create an org session instead — the server provisions (or reuses) `recoupable/`. The caller never constructs repo URLs.", "properties": { "title": { "type": "string", "description": "Display title for the session. When omitted, the server generates one." }, "organizationId": { "type": "string", "format": "uuid", "description": "Recoupable organization id. When provided, the session is created against the org's workspace repo (`recoupable/`) and the caller must have access to that organization. When omitted, the session is personal and uses the caller's own workspace repo (`recoupable/`)." }, "artistId": { "type": "string", "format": "uuid", "description": "Artist account id to associate the session with. When provided, the session is created in the context of that artist — used by the chat sidebar to filter chats by artist. Optional; omit for a session with no artist context." } } }, "CreateSessionResponse": { "type": "object", "required": [ "session", "chat" ], "properties": { "session": { "$ref": "#/components/schemas/Session" }, "chat": { "$ref": "#/components/schemas/Chat" } } }, "Error": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `\"error\"` for error responses." }, "error": { "type": "string", "description": "Human-readable error message." } } }, "Session": { "type": "object", "description": "Agent session returned by [`POST /api/sessions`](/docs/api-reference/sessions/create), [`GET /api/sessions/{sessionId}`](/docs/api-reference/sessions/get), and [`PATCH /api/sessions/{sessionId}`](/docs/api-reference/sessions/patch). The api serializes every field listed in `required` on each response, including `isNewBranch` (boolean, from the non-null `sessions.is_new_branch` column) and `artistId` (UUID or null).", "required": [ "id", "userId", "artistId", "title", "status", "isNewBranch", "globalSkillRefs", "lifecycleVersion", "createdAt", "updatedAt" ], "properties": { "id": { "type": "string", "description": "Session id (nanoid)." }, "userId": { "type": "string", "format": "uuid", "description": "Owning account id. Named `userId` here for compatibility with existing clients; on the server side this is the account_id foreign key into the accounts table." }, "title": { "type": "string" }, "status": { "type": "string", "enum": [ "running", "completed", "failed", "archived" ] }, "repoOwner": { "type": "string", "nullable": true, "description": "GitHub repo owner if the session is bound to a repository." }, "repoName": { "type": "string", "nullable": true }, "branch": { "type": "string", "nullable": true }, "cloneUrl": { "type": "string", "nullable": true, "description": "Clone URL the sandbox should fetch from." }, "isNewBranch": { "type": "boolean", "description": "Always present on session responses. True when this session created and pushed a new branch (not committing back to the original); false otherwise." }, "globalSkillRefs": { "type": "array", "description": "Skills attached to the agent at provision time.", "items": { "type": "object", "additionalProperties": true } }, "sandboxState": { "type": "object", "nullable": true, "additionalProperties": true, "description": "Sandbox runtime state (Vercel Sandbox)." }, "lifecycleState": { "type": "string", "nullable": true, "enum": [ "provisioning", "active", "hibernating", "hibernated", "restoring", "archived", "failed" ], "description": "Lifecycle orchestration state for the sandbox." }, "lifecycleVersion": { "type": "integer", "description": "Optimistic concurrency token for lifecycle transitions." }, "lastActivityAt": { "type": "string", "format": "date-time", "nullable": true }, "sandboxExpiresAt": { "type": "string", "format": "date-time", "nullable": true }, "hibernateAfter": { "type": "string", "format": "date-time", "nullable": true }, "lifecycleRunId": { "type": "string", "nullable": true }, "lifecycleError": { "type": "string", "nullable": true }, "linesAdded": { "type": "integer", "nullable": true, "description": "Lines added across the session's diff. Defaults to 0; null when stats have not been computed." }, "linesRemoved": { "type": "integer", "nullable": true, "description": "Lines removed across the session's diff. Defaults to 0; null when stats have not been computed." }, "snapshotUrl": { "type": "string", "nullable": true }, "snapshotCreatedAt": { "type": "string", "format": "date-time", "nullable": true }, "snapshotSizeBytes": { "type": "integer", "nullable": true }, "cachedDiff": { "type": "object", "nullable": true, "additionalProperties": true }, "cachedDiffUpdatedAt": { "type": "string", "format": "date-time", "nullable": true }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "artistId": { "type": [ "string", "null" ], "format": "uuid", "description": "Artist account id this session was created in the context of, or `null` when no artist was associated. Set via `POST /api/sessions { artistId }`; surfaces here for clients (e.g. the chat sidebar) that filter sessions/chats by artist." } } }, "Chat": { "type": "object", "required": [ "id", "sessionId", "title", "modelId", "createdAt", "updatedAt" ], "properties": { "id": { "type": "string", "description": "Chat id (nanoid)." }, "sessionId": { "type": "string", "description": "Owning session id." }, "title": { "type": "string", "description": "Display title for the chat. The initial chat created with a session is titled `New chat`." }, "modelId": { "type": "string", "description": "AI Gateway model identifier the chat is configured to use (e.g. `openai/gpt-5.4`)." }, "activeStreamId": { "type": "string", "nullable": true, "description": "Id of an in-flight assistant stream, if one is active." }, "lastAssistantMessageAt": { "type": "string", "format": "date-time", "nullable": true, "description": "Timestamp of the most recent assistant message in this chat." }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } } } }, "securitySchemes": { "ApiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "BearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Session Source: https://recoupable.dev/docs/api-reference/sessions/get Get session by id ## GET /api/sessions/{sessionId} Full OpenAPI specification: https://recoupable.dev/docs/spec/sessions.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Sessions", "description": "Sessions — sandboxed runs of an LLM-driven agent with tool use, lifecycle hooks, and persistence. Each session pairs one Vercel Sandbox with one or more chat threads.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "security": [ { "ApiKeyAuth": [] }, { "BearerAuth": [] } ], "paths": { "/api/sessions/{sessionId}": { "get": { "summary": "Get session by id", "description": "Returns a single agent session by its id.", "parameters": [ { "name": "sessionId", "in": "path", "required": true, "description": "The id of the session to fetch.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Session retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GetSessionResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing API key / Bearer token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden - the authenticated account does not own this session", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found - no session exists with the given id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "GetSessionResponse": { "type": "object", "required": [ "session" ], "properties": { "session": { "$ref": "#/components/schemas/Session" } } }, "Error": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `\"error\"` for error responses." }, "error": { "type": "string", "description": "Human-readable error message." } } }, "Session": { "type": "object", "description": "Agent session returned by [`POST /api/sessions`](/docs/api-reference/sessions/create), [`GET /api/sessions/{sessionId}`](/docs/api-reference/sessions/get), and [`PATCH /api/sessions/{sessionId}`](/docs/api-reference/sessions/patch). The api serializes every field listed in `required` on each response, including `isNewBranch` (boolean, from the non-null `sessions.is_new_branch` column) and `artistId` (UUID or null).", "required": [ "id", "userId", "artistId", "title", "status", "isNewBranch", "globalSkillRefs", "lifecycleVersion", "createdAt", "updatedAt" ], "properties": { "id": { "type": "string", "description": "Session id (nanoid)." }, "userId": { "type": "string", "format": "uuid", "description": "Owning account id. Named `userId` here for compatibility with existing clients; on the server side this is the account_id foreign key into the accounts table." }, "title": { "type": "string" }, "status": { "type": "string", "enum": [ "running", "completed", "failed", "archived" ] }, "repoOwner": { "type": "string", "nullable": true, "description": "GitHub repo owner if the session is bound to a repository." }, "repoName": { "type": "string", "nullable": true }, "branch": { "type": "string", "nullable": true }, "cloneUrl": { "type": "string", "nullable": true, "description": "Clone URL the sandbox should fetch from." }, "isNewBranch": { "type": "boolean", "description": "Always present on session responses. True when this session created and pushed a new branch (not committing back to the original); false otherwise." }, "globalSkillRefs": { "type": "array", "description": "Skills attached to the agent at provision time.", "items": { "type": "object", "additionalProperties": true } }, "sandboxState": { "type": "object", "nullable": true, "additionalProperties": true, "description": "Sandbox runtime state (Vercel Sandbox)." }, "lifecycleState": { "type": "string", "nullable": true, "enum": [ "provisioning", "active", "hibernating", "hibernated", "restoring", "archived", "failed" ], "description": "Lifecycle orchestration state for the sandbox." }, "lifecycleVersion": { "type": "integer", "description": "Optimistic concurrency token for lifecycle transitions." }, "lastActivityAt": { "type": "string", "format": "date-time", "nullable": true }, "sandboxExpiresAt": { "type": "string", "format": "date-time", "nullable": true }, "hibernateAfter": { "type": "string", "format": "date-time", "nullable": true }, "lifecycleRunId": { "type": "string", "nullable": true }, "lifecycleError": { "type": "string", "nullable": true }, "linesAdded": { "type": "integer", "nullable": true, "description": "Lines added across the session's diff. Defaults to 0; null when stats have not been computed." }, "linesRemoved": { "type": "integer", "nullable": true, "description": "Lines removed across the session's diff. Defaults to 0; null when stats have not been computed." }, "snapshotUrl": { "type": "string", "nullable": true }, "snapshotCreatedAt": { "type": "string", "format": "date-time", "nullable": true }, "snapshotSizeBytes": { "type": "integer", "nullable": true }, "cachedDiff": { "type": "object", "nullable": true, "additionalProperties": true }, "cachedDiffUpdatedAt": { "type": "string", "format": "date-time", "nullable": true }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "artistId": { "type": [ "string", "null" ], "format": "uuid", "description": "Artist account id this session was created in the context of, or `null` when no artist was associated. Set via `POST /api/sessions { artistId }`; surfaces here for clients (e.g. the chat sidebar) that filter sessions/chats by artist." } } } }, "securitySchemes": { "ApiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "BearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Update Session Source: https://recoupable.dev/docs/api-reference/sessions/patch Rename a session, change lifecycle status, or update line counters. ## PATCH /api/sessions/{sessionId} Full OpenAPI specification: https://recoupable.dev/docs/spec/sessions.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Sessions", "description": "Sessions — sandboxed runs of an LLM-driven agent with tool use, lifecycle hooks, and persistence. Each session pairs one Vercel Sandbox with one or more chat threads.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "security": [ { "ApiKeyAuth": [] }, { "BearerAuth": [] } ], "paths": { "/api/sessions/{sessionId}": { "patch": { "operationId": "patchSessionById", "summary": "Update session", "description": "Renames a session or changes its status (e.g. archive / unarchive). All body fields are optional; omitted fields are left unchanged.", "parameters": [ { "name": "sessionId", "in": "path", "required": true, "description": "The id of the session to update.", "schema": { "type": "string" } } ], "requestBody": { "required": false, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PatchSessionBody" } } } }, "responses": { "200": { "description": "Session updated successfully.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GetSessionResponse" } } } }, "400": { "description": "Bad request — malformed JSON body.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized — invalid or missing API key / Bearer token.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden — the authenticated account does not own this session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found — no session exists with the given id.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal server error — the session could not be updated.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "PatchSessionBody": { "type": "object", "description": "All fields are optional; an empty object `{}` is valid and leaves the session unchanged.", "properties": { "title": { "type": "string", "description": "New display title for the session (rename)." }, "status": { "type": "string", "enum": [ "running", "completed", "failed", "archived" ], "description": "Lifecycle status (matches the DB CHECK). `running` is active / unarchived; `completed` and `failed` are terminal; `archived` archives the row. Use `running` to unarchive from `archived`." }, "linesAdded": { "type": "integer", "minimum": 0, "description": "Updates the persisted `lines_added` column on the session (≥ 0)." }, "linesRemoved": { "type": "integer", "minimum": 0, "description": "Updates the persisted `lines_removed` column on the session (≥ 0)." } } }, "GetSessionResponse": { "type": "object", "required": [ "session" ], "properties": { "session": { "$ref": "#/components/schemas/Session" } } }, "Error": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `\"error\"` for error responses." }, "error": { "type": "string", "description": "Human-readable error message." } } }, "Session": { "type": "object", "description": "Agent session returned by [`POST /api/sessions`](/docs/api-reference/sessions/create), [`GET /api/sessions/{sessionId}`](/docs/api-reference/sessions/get), and [`PATCH /api/sessions/{sessionId}`](/docs/api-reference/sessions/patch). The api serializes every field listed in `required` on each response, including `isNewBranch` (boolean, from the non-null `sessions.is_new_branch` column) and `artistId` (UUID or null).", "required": [ "id", "userId", "artistId", "title", "status", "isNewBranch", "globalSkillRefs", "lifecycleVersion", "createdAt", "updatedAt" ], "properties": { "id": { "type": "string", "description": "Session id (nanoid)." }, "userId": { "type": "string", "format": "uuid", "description": "Owning account id. Named `userId` here for compatibility with existing clients; on the server side this is the account_id foreign key into the accounts table." }, "title": { "type": "string" }, "status": { "type": "string", "enum": [ "running", "completed", "failed", "archived" ] }, "repoOwner": { "type": "string", "nullable": true, "description": "GitHub repo owner if the session is bound to a repository." }, "repoName": { "type": "string", "nullable": true }, "branch": { "type": "string", "nullable": true }, "cloneUrl": { "type": "string", "nullable": true, "description": "Clone URL the sandbox should fetch from." }, "isNewBranch": { "type": "boolean", "description": "Always present on session responses. True when this session created and pushed a new branch (not committing back to the original); false otherwise." }, "globalSkillRefs": { "type": "array", "description": "Skills attached to the agent at provision time.", "items": { "type": "object", "additionalProperties": true } }, "sandboxState": { "type": "object", "nullable": true, "additionalProperties": true, "description": "Sandbox runtime state (Vercel Sandbox)." }, "lifecycleState": { "type": "string", "nullable": true, "enum": [ "provisioning", "active", "hibernating", "hibernated", "restoring", "archived", "failed" ], "description": "Lifecycle orchestration state for the sandbox." }, "lifecycleVersion": { "type": "integer", "description": "Optimistic concurrency token for lifecycle transitions." }, "lastActivityAt": { "type": "string", "format": "date-time", "nullable": true }, "sandboxExpiresAt": { "type": "string", "format": "date-time", "nullable": true }, "hibernateAfter": { "type": "string", "format": "date-time", "nullable": true }, "lifecycleRunId": { "type": "string", "nullable": true }, "lifecycleError": { "type": "string", "nullable": true }, "linesAdded": { "type": "integer", "nullable": true, "description": "Lines added across the session's diff. Defaults to 0; null when stats have not been computed." }, "linesRemoved": { "type": "integer", "nullable": true, "description": "Lines removed across the session's diff. Defaults to 0; null when stats have not been computed." }, "snapshotUrl": { "type": "string", "nullable": true }, "snapshotCreatedAt": { "type": "string", "format": "date-time", "nullable": true }, "snapshotSizeBytes": { "type": "integer", "nullable": true }, "cachedDiff": { "type": "object", "nullable": true, "additionalProperties": true }, "cachedDiffUpdatedAt": { "type": "string", "format": "date-time", "nullable": true }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "artistId": { "type": [ "string", "null" ], "format": "uuid", "description": "Artist account id this session was created in the context of, or `null` when no artist was associated. Set via `POST /api/sessions { artistId }`; surfaces here for clients (e.g. the chat sidebar) that filter sessions/chats by artist." } } } }, "securitySchemes": { "ApiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "BearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # List Session Chats Source: https://recoupable.dev/docs/api-reference/sessions/list-chats List session chats ## GET /api/sessions/{sessionId}/chats Full OpenAPI specification: https://recoupable.dev/docs/spec/sessions.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Sessions", "description": "Sessions — sandboxed runs of an LLM-driven agent with tool use, lifecycle hooks, and persistence. Each session pairs one Vercel Sandbox with one or more chat threads.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "security": [ { "ApiKeyAuth": [] }, { "BearerAuth": [] } ], "paths": { "/api/sessions/{sessionId}/chats": { "get": { "summary": "List session chats", "description": "Lists every chat in the given session as a `ChatSummary` (chat row plus per-account `hasUnread` and `isStreaming` flags), along with the caller's default model id. Chats are sorted by `createdAt` ascending.", "parameters": [ { "name": "sessionId", "in": "path", "required": true, "description": "The id of the parent session.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Chats retrieved successfully.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ListSessionChatsResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key / Bearer token.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden — the authenticated account does not own this session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found — no session exists with the given id.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "ListSessionChatsResponse": { "type": "object", "required": [ "chats", "defaultModelId" ], "properties": { "chats": { "type": "array", "description": "Every chat in the session, sorted by `createdAt` ascending.", "items": { "$ref": "#/components/schemas/ChatSummary" } }, "defaultModelId": { "type": "string", "description": "Default model id surfaced to clients with no explicit preference (e.g. `openai/gpt-5.4`)." } } }, "Error": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `\"error\"` for error responses." }, "error": { "type": "string", "description": "Human-readable error message." } } }, "ChatSummary": { "type": "object", "description": "Chat row enriched with per-account `hasUnread` and computed `isStreaming` flags for chat-list rendering.", "required": [ "id", "sessionId", "title", "createdAt", "updatedAt", "hasUnread", "isStreaming" ], "properties": { "id": { "type": "string" }, "sessionId": { "type": "string" }, "title": { "type": "string" }, "modelId": { "type": "string", "nullable": true }, "activeStreamId": { "type": "string", "nullable": true }, "lastAssistantMessageAt": { "type": "string", "format": "date-time", "nullable": true }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "hasUnread": { "type": "boolean", "description": "True when `lastAssistantMessageAt` is newer than the caller's `chat_reads.last_read_at` (or no read row exists yet)." }, "isStreaming": { "type": "boolean", "description": "True when `activeStreamId` is non-null." } } } }, "securitySchemes": { "ApiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "BearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Create Session Chat Source: https://recoupable.dev/docs/api-reference/sessions/create-chat Create session chat ## POST /api/sessions/{sessionId}/chats Full OpenAPI specification: https://recoupable.dev/docs/spec/sessions.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Sessions", "description": "Sessions — sandboxed runs of an LLM-driven agent with tool use, lifecycle hooks, and persistence. Each session pairs one Vercel Sandbox with one or more chat threads.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "security": [ { "ApiKeyAuth": [] }, { "BearerAuth": [] } ], "paths": { "/api/sessions/{sessionId}/chats": { "post": { "summary": "Create session chat", "description": "Creates a new chat inside the given session. Callers may pass `{ id }` to claim a deterministic chat id — useful for optimistic UI flows where the client generates the id locally and then persists it. If a chat with that id already exists in **this** session the call is idempotent and returns the existing row; if it exists in **another** session, 409 is returned.", "parameters": [ { "name": "sessionId", "in": "path", "required": true, "description": "The id of the parent session.", "schema": { "type": "string" } } ], "requestBody": { "required": false, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSessionChatRequest" } } } }, "responses": { "200": { "description": "Chat created, or existing chat returned (idempotent on same-session reuse).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSessionChatResponse" } } } }, "400": { "description": "Invalid chat id — body contained `id` but it was an empty string or not a string.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InvalidChatIdError" } } } }, "401": { "description": "Unauthorized — invalid or missing API key / Bearer token.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden — the authenticated account does not own this session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found — no session exists with the given id.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Chat id conflict — a chat with the requested id already exists on a different session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatIdConflictError" } } } }, "500": { "description": "Server error — the chat could not be persisted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "CreateSessionChatRequest": { "type": "object", "description": "Body for `POST /api/sessions/{sessionId}/chats`. Both an empty body and an omitted body are valid.", "properties": { "id": { "type": "string", "minLength": 1, "description": "Optional client-supplied chat id (used for optimistic UI flows). When omitted, the server generates a UUID. When supplied, must be a non-empty string." } } }, "CreateSessionChatResponse": { "type": "object", "required": [ "chat" ], "properties": { "chat": { "$ref": "#/components/schemas/Chat" } } }, "InvalidChatIdError": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "enum": [ "Invalid chat id" ] } } }, "Error": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `\"error\"` for error responses." }, "error": { "type": "string", "description": "Human-readable error message." } } }, "ChatIdConflictError": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "enum": [ "Chat ID conflict" ] } } }, "Chat": { "type": "object", "required": [ "id", "sessionId", "title", "modelId", "createdAt", "updatedAt" ], "properties": { "id": { "type": "string", "description": "Chat id (nanoid)." }, "sessionId": { "type": "string", "description": "Owning session id." }, "title": { "type": "string", "description": "Display title for the chat. The initial chat created with a session is titled `New chat`." }, "modelId": { "type": "string", "description": "AI Gateway model identifier the chat is configured to use (e.g. `openai/gpt-5.4`)." }, "activeStreamId": { "type": "string", "nullable": true, "description": "Id of an in-flight assistant stream, if one is active." }, "lastAssistantMessageAt": { "type": "string", "format": "date-time", "nullable": true, "description": "Timestamp of the most recent assistant message in this chat." }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } } } }, "securitySchemes": { "ApiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "BearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Session Chat Source: https://recoupable.dev/docs/api-reference/sessions/get-chat Get session chat ## GET /api/sessions/{sessionId}/chats/{chatId} Full OpenAPI specification: https://recoupable.dev/docs/spec/sessions.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Sessions", "description": "Sessions — sandboxed runs of an LLM-driven agent with tool use, lifecycle hooks, and persistence. Each session pairs one Vercel Sandbox with one or more chat threads.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "security": [ { "ApiKeyAuth": [] }, { "BearerAuth": [] } ], "paths": { "/api/sessions/{sessionId}/chats/{chatId}": { "get": { "summary": "Get session chat", "description": "Returns the chat's persisted UI message stream plus its current streaming state so callers can hydrate or refresh a chat view. `messages` is an array of `parts` payloads — one per `chat_messages` row — ordered by `created_at` ascending (ties broken by id). `isStreaming` is derived from `activeStreamId`.", "parameters": [ { "name": "sessionId", "in": "path", "required": true, "description": "The id of the parent session.", "schema": { "type": "string" } }, { "name": "chatId", "in": "path", "required": true, "description": "The id of the chat being fetched.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Chat retrieved successfully.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SessionChatResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key / Bearer token.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden — the authenticated account does not own this session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found — no session exists with the given id, the chat does not exist, or the chat belongs to a different session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "SessionChatResponse": { "type": "object", "description": "Response body for `GET /api/sessions/{sessionId}/chats/{chatId}`. Carries the full chat row plus its streaming state and persisted UI message stream — enough for both initial render and in-tab refresh.", "required": [ "chat", "isStreaming", "messages" ], "properties": { "chat": { "$ref": "#/components/schemas/Chat" }, "isStreaming": { "type": "boolean", "description": "True when `chat.activeStreamId` is non-null." }, "messages": { "type": "array", "description": "Persisted UI message stream — one serialized `UIMessage` per `chat_messages` row, ordered by `created_at` ascending (ties broken by `id`). Each item is the full message object, not a bare parts array.", "items": { "type": "object", "description": "A serialized `UIMessage` as stored by the agent.", "required": [ "id", "role", "parts" ], "properties": { "id": { "type": "string", "description": "Message id." }, "role": { "type": "string", "enum": [ "user", "assistant" ] }, "parts": { "type": "array", "description": "Ordered message parts (text, tool calls, data parts, etc.).", "items": { "type": "object", "additionalProperties": true } } }, "additionalProperties": true } } } }, "Error": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `\"error\"` for error responses." }, "error": { "type": "string", "description": "Human-readable error message." } } }, "Chat": { "type": "object", "required": [ "id", "sessionId", "title", "modelId", "createdAt", "updatedAt" ], "properties": { "id": { "type": "string", "description": "Chat id (nanoid)." }, "sessionId": { "type": "string", "description": "Owning session id." }, "title": { "type": "string", "description": "Display title for the chat. The initial chat created with a session is titled `New chat`." }, "modelId": { "type": "string", "description": "AI Gateway model identifier the chat is configured to use (e.g. `openai/gpt-5.4`)." }, "activeStreamId": { "type": "string", "nullable": true, "description": "Id of an in-flight assistant stream, if one is active." }, "lastAssistantMessageAt": { "type": "string", "format": "date-time", "nullable": true, "description": "Timestamp of the most recent assistant message in this chat." }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } } } }, "securitySchemes": { "ApiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "BearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Update Session Chat Source: https://recoupable.dev/docs/api-reference/sessions/patch-chat Update session chat ## PATCH /api/sessions/{sessionId}/chats/{chatId} Full OpenAPI specification: https://recoupable.dev/docs/spec/sessions.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Sessions", "description": "Sessions — sandboxed runs of an LLM-driven agent with tool use, lifecycle hooks, and persistence. Each session pairs one Vercel Sandbox with one or more chat threads.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "security": [ { "ApiKeyAuth": [] }, { "BearerAuth": [] } ], "paths": { "/api/sessions/{sessionId}/chats/{chatId}": { "patch": { "summary": "Update session chat", "description": "Applies a partial update to the chat. Body must include at least one of `title` or `modelId` and any provided value must be a non-empty string (whitespace is trimmed for `title`).", "parameters": [ { "name": "sessionId", "in": "path", "required": true, "description": "The id of the parent session.", "schema": { "type": "string" } }, { "name": "chatId", "in": "path", "required": true, "description": "The id of the chat being updated.", "schema": { "type": "string" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateSessionChatRequest" } } } }, "responses": { "200": { "description": "Chat updated successfully.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateSessionChatResponse" } } } }, "400": { "description": "Invalid body — JSON parse failed, neither `title` nor `modelId` was provided, or one of them was empty.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized — invalid or missing API key / Bearer token.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden — the authenticated account does not own this session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found — no session exists with the given id, the chat does not exist, or the chat belongs to a different session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Server error — the chat could not be updated.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "UpdateSessionChatRequest": { "type": "object", "description": "Body for `PATCH /api/sessions/{sessionId}/chats/{chatId}`. At least one of `title` or `modelId` must be provided; whichever is provided is trimmed and must be non-empty after trimming.", "additionalProperties": false, "anyOf": [ { "required": [ "title" ] }, { "required": [ "modelId" ] } ], "properties": { "title": { "type": "string", "minLength": 1, "pattern": "\\S", "description": "New display title for the chat. Trimmed; must be non-empty after trimming." }, "modelId": { "type": "string", "minLength": 1, "pattern": "\\S", "description": "AI Gateway model identifier the chat should be configured to use. Trimmed; must be non-empty after trimming." } } }, "UpdateSessionChatResponse": { "type": "object", "required": [ "chat" ], "properties": { "chat": { "$ref": "#/components/schemas/Chat" } } }, "Error": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `\"error\"` for error responses." }, "error": { "type": "string", "description": "Human-readable error message." } } }, "Chat": { "type": "object", "required": [ "id", "sessionId", "title", "modelId", "createdAt", "updatedAt" ], "properties": { "id": { "type": "string", "description": "Chat id (nanoid)." }, "sessionId": { "type": "string", "description": "Owning session id." }, "title": { "type": "string", "description": "Display title for the chat. The initial chat created with a session is titled `New chat`." }, "modelId": { "type": "string", "description": "AI Gateway model identifier the chat is configured to use (e.g. `openai/gpt-5.4`)." }, "activeStreamId": { "type": "string", "nullable": true, "description": "Id of an in-flight assistant stream, if one is active." }, "lastAssistantMessageAt": { "type": "string", "format": "date-time", "nullable": true, "description": "Timestamp of the most recent assistant message in this chat." }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } } } }, "securitySchemes": { "ApiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "BearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Delete Session Chat Source: https://recoupable.dev/docs/api-reference/sessions/delete-chat Delete session chat ## DELETE /api/sessions/{sessionId}/chats/{chatId} Full OpenAPI specification: https://recoupable.dev/docs/spec/sessions.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Sessions", "description": "Sessions — sandboxed runs of an LLM-driven agent with tool use, lifecycle hooks, and persistence. Each session pairs one Vercel Sandbox with one or more chat threads.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "security": [ { "ApiKeyAuth": [] }, { "BearerAuth": [] } ], "paths": { "/api/sessions/{sessionId}/chats/{chatId}": { "delete": { "summary": "Delete session chat", "description": "Removes the chat (cascade clears `chat_messages` and `chat_reads`). Refuses with 400 if this is the only chat in the session — sessions must always retain at least one chat.", "parameters": [ { "name": "sessionId", "in": "path", "required": true, "description": "The id of the parent session.", "schema": { "type": "string" } }, { "name": "chatId", "in": "path", "required": true, "description": "The id of the chat being deleted.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Chat deleted successfully.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteSessionChatResponse" } } } }, "400": { "description": "Cannot delete the only chat in a session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized — invalid or missing API key / Bearer token.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden — the authenticated account does not own this session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found — no session exists with the given id, the chat does not exist, or the chat belongs to a different session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Server error — the chat could not be deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "DeleteSessionChatResponse": { "type": "object", "required": [ "success" ], "properties": { "success": { "type": "boolean", "enum": [ true ] } } }, "Error": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `\"error\"` for error responses." }, "error": { "type": "string", "description": "Human-readable error message." } } } }, "securitySchemes": { "ApiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "BearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Mark Chat Read Source: https://recoupable.dev/docs/api-reference/sessions/mark-chat-read Marks a session chat as read for the authenticated account. ## POST /api/sessions/{sessionId}/chats/{chatId}/read Full OpenAPI specification: https://recoupable.dev/docs/spec/sessions.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Sessions", "description": "Sessions — sandboxed runs of an LLM-driven agent with tool use, lifecycle hooks, and persistence. Each session pairs one Vercel Sandbox with one or more chat threads.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "security": [ { "ApiKeyAuth": [] }, { "BearerAuth": [] } ], "paths": { "/api/sessions/{sessionId}/chats/{chatId}/read": { "post": { "operationId": "markChatRead", "summary": "Mark chat as read", "description": "Records that the authenticated account has read this chat up to the current timestamp. This upserts a row in `chat_reads` setting `last_read_at = now()`. After a successful call the `hasUnread` flag for this chat will be `false` in subsequent `GET /api/sessions/{sessionId}/chats` responses.", "parameters": [ { "name": "sessionId", "in": "path", "required": true, "description": "The id of the parent session.", "schema": { "type": "string" } }, { "name": "chatId", "in": "path", "required": true, "description": "The id of the chat to mark as read.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Chat marked as read successfully.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MarkChatReadResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key / Bearer token.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden — the authenticated account does not own this session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found — no session exists with the given id, the chat does not exist, or the chat belongs to a different session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Server error — the read record could not be persisted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "MarkChatReadResponse": { "type": "object", "required": [ "success" ], "properties": { "success": { "type": "boolean", "enum": [ true ] } } }, "Error": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `\"error\"` for error responses." }, "error": { "type": "string", "description": "Human-readable error message." } } } }, "securitySchemes": { "ApiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "BearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Create or restore session sandbox Source: https://recoupable.dev/docs/api-reference/sandbox/create Create or restore session sandbox ## POST /api/sandbox Full OpenAPI specification: https://recoupable.dev/docs/spec/sandbox.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Session Sandboxes", "description": "Per-session sandbox lifecycle for agent runs. These endpoints provision, restore, and report status on the Sandbox bound to a single agent session. Distinct from the legacy `/api/sandboxes` (plural) account-scoped endpoints — these are session-scoped and used by the open-agents-style chat UI to drive the \"loading sandbox…\" state on session entry.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "security": [ { "ApiKeyAuth": [] }, { "BearerAuth": [] } ], "paths": { "/api/sandbox": { "post": { "summary": "Create or restore session sandbox", "description": "Provisions a Sandbox for the given session. If a per-org base snapshot exists, the sandbox boots from it (skipping the full repo clone, ~75s saved). Otherwise the sandbox boots from the default base snapshot and a background workflow builds an org-specific snapshot for next time. When the session has prior runtime state (a paused or running sandbox under the same `sandboxName`), the call resumes it instead of creating a new one. On success, the session row is updated with the new `sandboxState` and lifecycle is bumped to `active`; the lifecycle workflow is kicked to manage hibernation and expiry from there.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSandboxRequest" } } } }, "responses": { "200": { "description": "Sandbox provisioned and bound to the session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSandboxResponse" } } } }, "400": { "description": "Invalid request body — malformed JSON, missing required fields, or invalid GitHub repository URL.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized — invalid or missing API key / Bearer token.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden — the authenticated account does not own the supplied `sessionId`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found — no session exists with the given `sessionId`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "502": { "description": "Upstream error — the sandbox provider failed to provision a sandbox.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "CreateSandboxRequest": { "type": "object", "required": [ "repoUrl" ], "properties": { "repoUrl": { "type": "string", "description": "GitHub repository URL the sandbox should clone (e.g. `https://github.com/owner/repo`). Validated against GitHub URL rules; private repos require the service GitHub token configured server-side." }, "sessionId": { "type": "string", "description": "Owning session id. Required for the chat UX flow — the sandbox is named deterministically from the session id, enabling resume across reconnects. When omitted, a one-shot ephemeral sandbox is created (legacy)." } } }, "CreateSandboxResponse": { "type": "object", "required": [ "createdAt", "timeout", "currentBranch", "mode", "timing" ], "properties": { "createdAt": { "type": "integer", "format": "int64", "description": "Epoch milliseconds when the sandbox handle was returned." }, "timeout": { "type": "integer", "format": "int64", "description": "Sandbox idle-timeout in milliseconds. The lifecycle workflow uses this to schedule hibernation." }, "currentBranch": { "type": "string", "description": "Branch the sandbox checked out." }, "mode": { "type": "string", "enum": [ "vercel" ], "description": "Sandbox provider. Currently always `vercel`." }, "timing": { "type": "object", "required": [ "readyMs" ], "properties": { "readyMs": { "type": "integer", "format": "int64", "description": "Wall-clock milliseconds from request receipt until the sandbox was ready. Useful for tracking cold-start vs warm-resume performance." } } } } }, "Error": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `\"error\"` for error responses." }, "error": { "type": "string", "description": "Human-readable error message." } } } }, "securitySchemes": { "ApiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "BearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get session sandbox status Source: https://recoupable.dev/docs/api-reference/sandbox/status Get session sandbox status ## GET /api/sandbox/status Full OpenAPI specification: https://recoupable.dev/docs/spec/sandbox.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Session Sandboxes", "description": "Per-session sandbox lifecycle for agent runs. These endpoints provision, restore, and report status on the Sandbox bound to a single agent session. Distinct from the legacy `/api/sandboxes` (plural) account-scoped endpoints — these are session-scoped and used by the open-agents-style chat UI to drive the \"loading sandbox…\" state on session entry.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "security": [ { "ApiKeyAuth": [] }, { "BearerAuth": [] } ], "paths": { "/api/sandbox/status": { "get": { "summary": "Get session sandbox status", "description": "Returns the current lifecycle and runtime state for the sandbox bound to a session. The chat UI polls this endpoint while showing the \"loading sandbox…\" state and flips to \"ready\" when `status` becomes `active`. The response includes `lifecycleVersion` (an optimistic concurrency token) and a `lifecycle` envelope with `serverTime`, the lifecycle FSM `state`, and timestamps for last activity, hibernation deadline, and sandbox expiry. As a side effect, if the runtime state is stale (expired or overdue for hibernation), the lifecycle workflow is kicked to clean up — callers do not need to do this themselves.", "parameters": [ { "name": "sessionId", "in": "query", "required": true, "description": "The id of the session whose sandbox status to read.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Sandbox status retrieved successfully.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SandboxStatusResponse" } } } }, "400": { "description": "Missing `sessionId` query parameter.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized — invalid or missing API key / Bearer token.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden — the authenticated account does not own this session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found — no session exists with the given `sessionId`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "SandboxStatusResponse": { "type": "object", "required": [ "status", "hasSnapshot", "lifecycleVersion", "lifecycle" ], "properties": { "status": { "type": "string", "enum": [ "active", "no_sandbox" ], "description": "`active` when a non-expired sandbox is bound to the session; `no_sandbox` otherwise. The chat UI flips out of its loading state when this becomes `active`." }, "hasSnapshot": { "type": "boolean", "description": "True when a paused/snapshotted sandbox exists and can be resumed. Used by the UI to decide whether to show \"resume\" vs \"create\" affordances when `status` is `no_sandbox`." }, "lifecycleVersion": { "type": "integer", "description": "Optimistic concurrency token for lifecycle transitions. Clients can pass this back to lifecycle-mutating endpoints to detect races." }, "lifecycle": { "$ref": "#/components/schemas/SandboxLifecycle" } } }, "Error": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `\"error\"` for error responses." }, "error": { "type": "string", "description": "Human-readable error message." } } }, "SandboxLifecycle": { "type": "object", "required": [ "serverTime", "state", "lastActivityAt", "hibernateAfter", "sandboxExpiresAt" ], "description": "Lifecycle envelope shared between `GET /api/sandbox/status` and `GET /api/sandbox/reconnect`. Server-clock-stamped snapshot of the sandbox's lifecycle FSM state and the timestamps the UI uses to render countdown timers.", "properties": { "serverTime": { "type": "integer", "format": "int64", "description": "Server's current epoch milliseconds. Use this — not the client clock — when computing how much time is left before `hibernateAfter` or `sandboxExpiresAt`." }, "state": { "type": "string", "nullable": true, "enum": [ "provisioning", "active", "hibernating", "hibernated", "restoring", "archived", "failed" ], "description": "Lifecycle FSM state. `null` for sessions that have never had a sandbox." }, "lastActivityAt": { "type": "integer", "format": "int64", "nullable": true, "description": "Epoch milliseconds of the last recorded sandbox activity, or null if there has been none." }, "hibernateAfter": { "type": "integer", "format": "int64", "nullable": true, "description": "Epoch milliseconds after which the sandbox is eligible for hibernation, or null when not applicable." }, "sandboxExpiresAt": { "type": "integer", "format": "int64", "nullable": true, "description": "Epoch milliseconds when the sandbox runtime expires, or null when not applicable." } } } }, "securitySchemes": { "ApiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "BearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Reconnect to session sandbox Source: https://recoupable.dev/docs/api-reference/sandbox/reconnect Reconnect to session sandbox ## GET /api/sandbox/reconnect Full OpenAPI specification: https://recoupable.dev/docs/spec/sandbox.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Session Sandboxes", "description": "Per-session sandbox lifecycle for agent runs. These endpoints provision, restore, and report status on the Sandbox bound to a single agent session. Distinct from the legacy `/api/sandboxes` (plural) account-scoped endpoints — these are session-scoped and used by the open-agents-style chat UI to drive the \"loading sandbox…\" state on session entry.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "security": [ { "ApiKeyAuth": [] }, { "BearerAuth": [] } ], "paths": { "/api/sandbox/reconnect": { "get": { "summary": "Reconnect to session sandbox", "description": "Live runtime probe for the sandbox bound to a session. Unlike `GET /api/sandbox/status` (DB-only read), this endpoint actually runs a quick command inside the sandbox to verify it is reachable. Used by the chat UI on session re-entry / tab refocus to decide whether to flip out of \"loading sandbox…\" or surface a \"resume\" affordance. Returns one of three operational outcomes via the `status` field: `\"connected\"` (sandbox is alive, included `expiresAt` reflects current expiry), `\"expired\"` (the runtime state is gone — the UI should offer to resume from snapshot if `hasSnapshot` is true, otherwise create a fresh sandbox), or `\"no_sandbox\"` (no sandbox has been provisioned for this session yet).", "parameters": [ { "name": "sessionId", "in": "query", "required": true, "description": "The id of the session whose sandbox to reconnect to.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Reconnect probe completed. Inspect `status` for the outcome.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReconnectResponse" } } } }, "400": { "description": "Missing `sessionId` query parameter.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized — invalid or missing API key / Bearer token.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden — the authenticated account does not own this session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Not found — no session exists with the given `sessionId`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "ReconnectResponse": { "type": "object", "required": [ "status", "hasSnapshot", "lifecycle" ], "properties": { "status": { "type": "string", "enum": [ "connected", "expired", "no_sandbox" ], "description": "`connected` when the live runtime probe succeeded; `expired` when the recorded runtime state is no longer reachable (the UI should offer resume-from-snapshot or fresh-create); `no_sandbox` when the session has never had a sandbox provisioned." }, "hasSnapshot": { "type": "boolean", "description": "True when a paused/snapshotted sandbox exists and can be resumed. Used by the UI to decide whether to show \"resume\" vs \"create\" affordances on `expired` / `no_sandbox`." }, "expiresAt": { "type": "integer", "format": "int64", "description": "Epoch milliseconds when the sandbox runtime will expire. Present only when `status` is `\"connected\"`; reflects the freshly-probed expiry, which may differ from `lifecycle.sandboxExpiresAt` if the sandbox extended itself between writes." }, "lifecycle": { "$ref": "#/components/schemas/SandboxLifecycle" } } }, "Error": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `\"error\"` for error responses." }, "error": { "type": "string", "description": "Human-readable error message." } } }, "SandboxLifecycle": { "type": "object", "required": [ "serverTime", "state", "lastActivityAt", "hibernateAfter", "sandboxExpiresAt" ], "description": "Lifecycle envelope shared between `GET /api/sandbox/status` and `GET /api/sandbox/reconnect`. Server-clock-stamped snapshot of the sandbox's lifecycle FSM state and the timestamps the UI uses to render countdown timers.", "properties": { "serverTime": { "type": "integer", "format": "int64", "description": "Server's current epoch milliseconds. Use this — not the client clock — when computing how much time is left before `hibernateAfter` or `sandboxExpiresAt`." }, "state": { "type": "string", "nullable": true, "enum": [ "provisioning", "active", "hibernating", "hibernated", "restoring", "archived", "failed" ], "description": "Lifecycle FSM state. `null` for sessions that have never had a sandbox." }, "lastActivityAt": { "type": "integer", "format": "int64", "nullable": true, "description": "Epoch milliseconds of the last recorded sandbox activity, or null if there has been none." }, "hibernateAfter": { "type": "integer", "format": "int64", "nullable": true, "description": "Epoch milliseconds after which the sandbox is eligible for hibernation, or null when not applicable." }, "sandboxExpiresAt": { "type": "integer", "format": "int64", "nullable": true, "description": "Epoch milliseconds when the sandbox runtime expires, or null when not applicable." } } } }, "securitySchemes": { "ApiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "BearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Account ID Source: https://recoupable.dev/docs/api-reference/accounts/id Retrieve the ID of the authenticated account associated with the provided credentials. This is useful when you have an API key or access token but do not yet know the corresponding accountId. ## GET /api/accounts/id Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/accounts/id": { "get": { "description": "Retrieve the ID of the authenticated account associated with the provided credentials. This is useful when you have an API key or access token but do not yet know the corresponding accountId.", "parameters": [], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Account ID retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GetAccountIdResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } } } } } }, "components": { "schemas": { "GetAccountIdResponse": { "type": "object", "required": [ "status", "accountId" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "accountId": { "type": "string", "format": "uuid", "description": "The unique identifier (UUID) of the authenticated account" } } }, "AccountErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Account Source: https://recoupable.dev/docs/api-reference/accounts/get Retrieve detailed account information by ID. Returns the account with associated profile info, emails, and wallet addresses. ## GET /api/accounts/{id} Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/accounts/{id}": { "get": { "description": "Retrieve detailed account information by ID. Returns the account with associated profile info, emails, and wallet addresses.", "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier (UUID) of the account", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Account retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GetAccountResponse" } } } }, "404": { "description": "Account not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } } } } } }, "components": { "schemas": { "GetAccountResponse": { "type": "object", "required": [ "status", "account" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "account": { "$ref": "#/components/schemas/Account", "description": "The account details" } } }, "AccountErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } }, "Account": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the account" }, "name": { "type": "string", "description": "Account display name" }, "image": { "type": "string", "nullable": true, "description": "Profile image URL" }, "instruction": { "type": "string", "nullable": true, "description": "Custom AI instructions for this account" }, "knowledges": { "type": "array", "nullable": true, "items": { "$ref": "#/components/schemas/Knowledge" }, "description": "Knowledge base files attached to this account" }, "email": { "type": "string", "nullable": true, "description": "Primary email address" }, "wallet_address": { "type": "string", "nullable": true, "description": "Connected wallet address" } } }, "Knowledge": { "type": "object", "properties": { "url": { "type": "string", "description": "URL to the knowledge file" }, "name": { "type": "string", "description": "Name of the knowledge file" }, "type": { "type": "string", "description": "MIME type of the file" } } } } } } ``` # Create Account Source: https://recoupable.dev/docs/api-reference/accounts/create Create a new account or retrieve an existing account by email or wallet address. If an account with the provided email or wallet already exists, returns that account. Otherwise creates a new account and initializes credits. ## POST /api/accounts Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/accounts": { "post": { "description": "Create a new account or retrieve an existing account by email or wallet address. If an account with the provided email or wallet already exists, returns that account. Otherwise creates a new account and initializes credits.", "requestBody": { "description": "Account credentials to create or lookup", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateAccountRequest" } } } }, "responses": { "200": { "description": "Account created or retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountDataResponse" } } } }, "400": { "description": "Bad request - failed to create account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } } } } } }, "components": { "schemas": { "CreateAccountRequest": { "type": "object", "properties": { "email": { "type": "string", "format": "email", "description": "Email address to associate with the account. If an account with this email exists, it will be returned." }, "wallet": { "type": "string", "description": "Wallet address to associate with the account. If an account with this wallet exists, it will be returned." } }, "description": "At least one of email or wallet should be provided to identify or create an account." }, "AccountDataResponse": { "type": "object", "required": [ "data" ], "properties": { "data": { "$ref": "#/components/schemas/AccountData", "description": "The account data" } } }, "AccountErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } }, "AccountData": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "The unique identifier of the account" }, "account_id": { "type": "string", "format": "uuid", "description": "The account ID (same as id, for consistency)" }, "name": { "type": "string", "description": "Display name of the account" }, "email": { "type": "string", "format": "email", "description": "Email address associated with the account" }, "wallet": { "type": "string", "description": "Wallet address associated with the account" }, "image": { "type": "string", "format": "uri", "description": "URL of the account's profile image" }, "instruction": { "type": "string", "description": "Custom instruction or bio" }, "organization": { "type": "string", "description": "Organization name" }, "job_title": { "type": "string", "description": "Job title" }, "role_type": { "type": "string", "description": "Role type" }, "company_name": { "type": "string", "description": "Company name" }, "knowledges": { "type": "array", "items": { "$ref": "#/components/schemas/Knowledge" }, "description": "Knowledge base files attached to this account" } } }, "Knowledge": { "type": "object", "properties": { "url": { "type": "string", "description": "URL to the knowledge file" }, "name": { "type": "string", "description": "Name of the knowledge file" }, "type": { "type": "string", "description": "MIME type of the file" } } } } } } ``` # Update Account Source: https://recoupable.dev/docs/api-reference/accounts/update Update an existing account's profile information including name, organization, image, instruction, job title, role type, company name, and knowledges. Requires authentication via x-api-key or Authorization Bearer token. The authenticated account may update itself or a permitted account_id override. ## PATCH /api/accounts Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/accounts": { "patch": { "description": "Update an existing account's profile information including name, organization, image, instruction, job title, role type, company name, and knowledges. Requires authentication via x-api-key or Authorization Bearer token. The authenticated account may update itself or a permitted account_id override.", "requestBody": { "description": "Account fields to update", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateAccountRequest" } } } }, "responses": { "200": { "description": "Account updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountDataResponse" } } } }, "400": { "description": "Bad request - account not found or update failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "403": { "description": "Forbidden - account override not permitted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } } } } } }, "components": { "schemas": { "UpdateAccountRequest": { "type": "object", "required": [ "accountId" ], "properties": { "accountId": { "type": "string", "format": "uuid", "description": "The unique identifier of the account to update" }, "name": { "type": "string", "description": "Display name for the account" }, "instruction": { "type": "string", "description": "Custom instruction or bio for the account" }, "organization": { "type": "string", "description": "Organization name associated with the account" }, "image": { "type": "string", "format": "uri", "description": "URL of the account's profile image" }, "jobTitle": { "type": "string", "description": "Job title of the account holder" }, "roleType": { "type": "string", "description": "Role type within the organization" }, "companyName": { "type": "string", "description": "Company name associated with the account" }, "knowledges": { "type": "array", "items": { "$ref": "#/components/schemas/Knowledge" }, "description": "Knowledge base files attached to this account" } } }, "AccountDataResponse": { "type": "object", "required": [ "data" ], "properties": { "data": { "$ref": "#/components/schemas/AccountData", "description": "The account data" } } }, "AccountErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } }, "Knowledge": { "type": "object", "properties": { "url": { "type": "string", "description": "URL to the knowledge file" }, "name": { "type": "string", "description": "Name of the knowledge file" }, "type": { "type": "string", "description": "MIME type of the file" } } }, "AccountData": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "The unique identifier of the account" }, "account_id": { "type": "string", "format": "uuid", "description": "The account ID (same as id, for consistency)" }, "name": { "type": "string", "description": "Display name of the account" }, "email": { "type": "string", "format": "email", "description": "Email address associated with the account" }, "wallet": { "type": "string", "description": "Wallet address associated with the account" }, "image": { "type": "string", "format": "uri", "description": "URL of the account's profile image" }, "instruction": { "type": "string", "description": "Custom instruction or bio" }, "organization": { "type": "string", "description": "Organization name" }, "job_title": { "type": "string", "description": "Job title" }, "role_type": { "type": "string", "description": "Role type" }, "company_name": { "type": "string", "description": "Company name" }, "knowledges": { "type": "array", "items": { "$ref": "#/components/schemas/Knowledge" }, "description": "Knowledge base files attached to this account" } } } } } } ``` # Add Artist to Account Source: https://recoupable.dev/docs/api-reference/accounts/add-artist Add an artist to an account's list of associated artists. If the artist is already associated with the account, returns success without modification. ## POST /api/accounts/artists Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/accounts/artists": { "post": { "description": "Add an artist to an account's list of associated artists. If the artist is already associated with the account, returns success without modification.", "requestBody": { "description": "Account and artist identifiers", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AddArtistToAccountRequest" } } } }, "responses": { "200": { "description": "Artist added to account successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AddArtistSuccessResponse" } } } }, "400": { "description": "Bad request - account not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } } } } } }, "components": { "schemas": { "AddArtistToAccountRequest": { "type": "object", "required": [ "email", "artistId" ], "properties": { "email": { "type": "string", "format": "email", "description": "Email address of the account to add the artist to" }, "artistId": { "type": "string", "format": "uuid", "description": "The unique identifier of the artist to add" } } }, "AddArtistSuccessResponse": { "type": "object", "required": [ "success" ], "properties": { "success": { "type": "boolean", "enum": [ true ], "description": "Indicates the artist was successfully added" } } }, "AccountErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } } } } } ``` # Create Subscription Session Source: https://recoupable.dev/docs/api-reference/subscriptions/sessions-create Create a checkout session to start a subscription for the authenticated account. Returns a hosted checkout URL that the client should redirect to. `plan` picks the price: `pro` (default) is pre-configured with a 30-day trial period; `starter` is charged at checkout with no trial. ## POST /api/subscriptions/sessions Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/subscriptions/sessions": { "post": { "description": "Create a checkout session to start a subscription for the authenticated account. Returns a hosted checkout URL that the client should redirect to. `plan` picks the price: `pro` (default) is pre-configured with a 30-day trial period; `starter` is charged at checkout with no trial.", "requestBody": { "description": "Session creation parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSubscriptionSessionRequest" } } } }, "responses": { "200": { "description": "Checkout session created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSubscriptionSessionResponse" } } } }, "400": { "description": "Bad request - missing or invalid parameters, or `plan` is `starter` while the Starter price is not configured (`error` is literally `starter_unavailable`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubscriptionSessionErrorResponse" }, "example": { "error": "starter_unavailable" } } } }, "401": { "description": "Unauthorized - an auth header was supplied but is invalid (omit it entirely for an anonymous session)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubscriptionSessionErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubscriptionSessionErrorResponse" } } } } } } } }, "components": { "schemas": { "CreateSubscriptionSessionRequest": { "type": "object", "required": [ "successUrl" ], "properties": { "plan": { "type": "string", "enum": [ "starter", "pro" ], "description": "The plan to buy. Defaults to `pro` when omitted (back-compat for authenticated callers). `pro` carries the 30-day trial; `starter` charges on completion.", "example": "pro" }, "successUrl": { "type": "string", "format": "uri", "description": "Where Stripe sends the buyer after payment. For anonymous checkout include the literal `{CHECKOUT_SESSION_ID}` placeholder so the client can call Claim Subscription if the sign-in email differs.", "example": "https://chat.recoupable.dev?checkout=success&session_id={CHECKOUT_SESSION_ID}" }, "cancelUrl": { "type": "string", "format": "uri", "description": "Where Stripe sends the buyer if they leave Checkout without paying.", "example": "https://recoupable.dev/pricing" } } }, "CreateSubscriptionSessionResponse": { "type": "object", "required": [ "id", "url" ], "properties": { "id": { "type": "string", "description": "The checkout session ID.", "example": "cs_test_a1b2c3d4e5f6g7h8i9j0" }, "url": { "type": "string", "format": "uri", "description": "The hosted checkout URL. Redirect to this URL to complete Checkout.", "example": "https://checkout.stripe.com/pay/cs_test_a1b2c3d4e5f6g7h8i9j0" } } }, "SubscriptionSessionErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Human-readable error message.", "example": "successUrl is required" } } } } } } ``` # Claim Subscription Source: https://recoupable.dev/docs/api-reference/subscriptions/claim-create Attach a subscription bought through [Create Subscription Session](/docs/api-reference/subscriptions/sessions-create) (anonymous path) to the authenticated account. Needed only when the signed-in email differs from the email typed into Stripe Checkout; when they match, the webhook already linked the subscription and this call is a no-op that returns 200. ## POST /api/subscriptions/claim Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/subscriptions/claim": { "post": { "description": "Attach a subscription bought through [Create Subscription Session](/docs/api-reference/subscriptions/sessions-create) (anonymous path) to the authenticated account. Needed only when the signed-in email differs from the email typed into Stripe Checkout; when they match, the webhook already linked the subscription and this call is a no-op that returns 200.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "The Checkout session to claim", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ClaimSubscriptionRequest" } } } }, "responses": { "200": { "description": "Subscription attached to the authenticated account (or already was)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ClaimSubscriptionResponse" } } } }, "400": { "description": "Bad request - missing `session_id`, or the session has no subscription", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ClaimSubscriptionErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ClaimSubscriptionErrorResponse" } } } }, "404": { "description": "Unknown Checkout session", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ClaimSubscriptionErrorResponse" }, "example": { "status": "error", "error": "session_not_found" } } } }, "409": { "description": "The subscription belongs to another account that has signed in", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ClaimSubscriptionErrorResponse" }, "example": { "status": "error", "error": "already_claimed" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ClaimSubscriptionErrorResponse" } } } } } } } }, "components": { "schemas": { "ClaimSubscriptionRequest": { "type": "object", "required": [ "session_id" ], "properties": { "session_id": { "type": "string", "description": "The Stripe Checkout session id from the `session_id` query parameter on the success URL.", "example": "cs_live_a1b2c3d4e5f6g7h8i9j0" } } }, "ClaimSubscriptionResponse": { "type": "object", "required": [ "status", "subscription_id", "plan" ], "properties": { "status": { "type": "string", "enum": [ "success" ] }, "subscription_id": { "type": "string", "description": "The Stripe subscription now attached to the authenticated account.", "example": "sub_1Rz0abc123" }, "plan": { "type": "string", "enum": [ "starter", "pro" ], "description": "The plan the subscription is on, resolved from its Stripe price.", "example": "pro" } } }, "ClaimSubscriptionErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ] }, "error": { "type": "string", "description": "Machine-readable code (`session_not_found`, `already_claimed`, `no_subscription`) or a validation message.", "example": "already_claimed" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Account Subscription Source: https://recoupable.dev/docs/api-reference/accounts/subscription-get Retrieve the subscription that confers an account's plan, directly or via an organization: `isPro`, `status`, `plan`, `source`, plus the billing fields `name`, `amountCents`, `currency`, `interval`, `collectionMethod` and `currentPeriodEnd`. Everything but `isPro` and `status` is null when `status` is `none`. `id` may be the authenticated account or an organization the caller belongs to. ## GET /api/accounts/{id}/subscription Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/accounts/{id}/subscription": { "get": { "description": "Retrieve the subscription that confers an account's plan, directly or via an organization: `isPro`, `status`, `plan`, `source`, plus the billing fields `name`, `amountCents`, `currency`, `interval`, `collectionMethod` and `currentPeriodEnd`. Everything but `isPro` and `status` is null when `status` is `none`. `id` may be the authenticated account or an organization the caller belongs to.", "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier (UUID) of the account. Must be the authenticated account or another accessible via organization membership.", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Subscription retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubscriptionResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubscriptionErrorResponse" }, "example": { "error": "Unauthorized" } } } }, "403": { "description": "Forbidden - account not accessible to the authenticated account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubscriptionErrorResponse" } } } }, "404": { "description": "Account not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubscriptionErrorResponse" } } } } } } } }, "components": { "schemas": { "SubscriptionResponse": { "type": "object", "required": [ "isPro", "status", "plan", "source", "name", "amountCents", "currency", "interval", "collectionMethod", "currentPeriodEnd" ], "properties": { "isPro": { "type": "boolean", "description": "True only for effective Pro entitlement. A paid Starter account returns false. Use plan for paid-plan decisions." }, "status": { "type": "string", "enum": [ "active", "trialing", "canceled", "past_due", "none" ], "description": "Current entitlement view. Resolving to Free returns none and null billing fields, including for an inactive subscription; this is not subscription lifecycle history.", "example": "active" }, "plan": { "type": [ "string", "null" ], "description": "Plan identifier (e.g. `pro`). Null when no subscription exists.", "example": "pro", "enum": [ "starter", "pro", null ] }, "source": { "type": [ "string", "null" ], "enum": [ "account", "organization", null ], "description": "Origin of the subscription. `account` for direct, `organization` when covered by an organization. Null when no subscription exists.", "example": "account" }, "name": { "type": [ "string", "null" ], "description": "Display name of the plan's Stripe price (nickname) or product. Null when no subscription exists.", "example": "Pro" }, "amountCents": { "type": [ "integer", "null" ], "description": "Recurring amount in the smallest currency unit (cents for USD). Null when no subscription exists.", "example": 9900 }, "currency": { "type": [ "string", "null" ], "description": "ISO 4217 currency code, lowercase.", "example": "usd" }, "interval": { "type": [ "string", "null" ], "enum": [ "day", "week", "month", "year", null ], "description": "Billing interval of the recurring price.", "example": "month" }, "collectionMethod": { "type": [ "string", "null" ], "enum": [ "charge_automatically", "send_invoice", null ], "description": "`charge_automatically` bills the card on file at renewal; `send_invoice` emails an invoice that is paid manually (enterprise plans). A saved card is never charged for a `send_invoice` plan.", "example": "charge_automatically" }, "currentPeriodEnd": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the current billing period: the renewal date, or the next invoice date for invoiced plans.", "example": "2026-10-04T00:00:00Z" } }, "description": "Effective subscription details. When status is none, plan, source, name, amountCents, currency, interval, collectionMethod and currentPeriodEnd are all null." }, "SubscriptionErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Human-readable error message.", "example": "Account not found" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Create Billing Portal Session Source: https://recoupable.dev/docs/api-reference/accounts/portal-create Create a Stripe Customer Portal session for an account. Returns a hosted URL where the customer can update the card on file, view invoices, or cancel a Starter or Pro subscription; the client should redirect the user to that URL. The portal opens on the Stripe customer that owns the account's active subscription, so accounts without an active subscription get a 400 even when a tagged customer with a card exists. `id` may be the authenticated account or an organization the caller belongs to. Enterprise (invoiced) plans are managed with your Recoup contact and the app does not offer this portal for them, but the endpoint works for any account with an active subscription. ## POST /api/accounts/{id}/portal Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/accounts/{id}/portal": { "post": { "description": "Create a Stripe Customer Portal session for an account. Returns a hosted URL where the customer can update the card on file, view invoices, or cancel a Starter or Pro subscription; the client should redirect the user to that URL. The portal opens on the Stripe customer that owns the account's active subscription, so accounts without an active subscription get a 400 even when a tagged customer with a card exists. `id` may be the authenticated account or an organization the caller belongs to. Enterprise (invoiced) plans are managed with your Recoup contact and the app does not offer this portal for them, but the endpoint works for any account with an active subscription.", "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier (UUID) of the account. Must be the authenticated account or another accessible via organization membership.", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Portal session parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSubscriptionPortalRequest" } } } }, "responses": { "200": { "description": "Portal session created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSubscriptionPortalResponse" } } } }, "400": { "description": "Bad request - invalid parameters, or the account has no active subscription", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubscriptionPortalErrorResponse" }, "example": { "error": "No active subscription found" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubscriptionPortalErrorResponse" }, "example": { "error": "Unauthorized" } } } }, "403": { "description": "Forbidden - the account is not the caller's and not an organization they belong to (an unknown id also returns 403, never 404)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubscriptionPortalErrorResponse" } } } } } } } }, "components": { "schemas": { "CreateSubscriptionPortalRequest": { "type": "object", "required": [ "returnUrl" ], "description": "Request body for creating a billing portal session. The account is taken from the `id` path parameter and must be accessible to the authenticated caller.", "properties": { "returnUrl": { "type": "string", "format": "uri", "description": "The URL to redirect to when the customer leaves the portal.", "example": "https://chat.recoupable.com/settings/billing" } } }, "CreateSubscriptionPortalResponse": { "type": "object", "required": [ "id", "url" ], "properties": { "id": { "type": "string", "description": "The portal session ID.", "example": "portal_sess_a1b2c3d4" }, "url": { "type": "string", "format": "uri", "description": "The hosted portal URL. Redirect to this URL so the customer can manage their subscription.", "example": "https://billing.example.com/manage/portal_sess_a1b2c3d4" } } }, "SubscriptionPortalErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Human-readable error message.", "example": "returnUrl is required" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Default Payment Method Source: https://recoupable.dev/docs/api-reference/accounts/payment-method-get Retrieve the default payment method on file for an account. An account may be tagged on more than one Stripe customer (for example an organization whose card was saved on one customer and whose invoiced plan lives on another); the card is read from the tagged customer that holds a default payment method. Returns `card: null` when no tagged customer has a payment method saved yet — the top-up dialog uses this to decide whether to show a pre-charge confirmation (card present) or route to a checkout session to collect one (`card: null`). Cards are returned even when expired; callers should compare `exp_month` / `exp_year` against the current date and warn the customer, since an off-session charge against an expired card will decline. ## GET /api/accounts/{id}/payment-method Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/accounts/{id}/payment-method": { "get": { "description": "Retrieve the default payment method on file for an account. An account may be tagged on more than one Stripe customer (for example an organization whose card was saved on one customer and whose invoiced plan lives on another); the card is read from the tagged customer that holds a default payment method. Returns `card: null` when no tagged customer has a payment method saved yet — the top-up dialog uses this to decide whether to show a pre-charge confirmation (card present) or route to a checkout session to collect one (`card: null`). Cards are returned even when expired; callers should compare `exp_month` / `exp_year` against the current date and warn the customer, since an off-session charge against an expired card will decline.", "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier (UUID) of the account. Must be the authenticated account or another accessible via organization membership.", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Default payment method retrieved successfully (may be `null` if none on file).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountPaymentMethodResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountPaymentMethodErrorResponse" }, "example": { "error": "Unauthorized" } } } }, "403": { "description": "Forbidden - the account is not the caller's and not an organization they belong to (an unknown id also returns 403, never 404)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountPaymentMethodErrorResponse" } } } } } } } }, "components": { "schemas": { "AccountPaymentMethodResponse": { "type": "object", "required": [ "account_id", "card" ], "properties": { "account_id": { "type": "string", "format": "uuid", "description": "The unique identifier of the account this payment method belongs to.", "example": "550e8400-e29b-41d4-a716-446655440000" }, "card": { "oneOf": [ { "$ref": "#/components/schemas/SavedCard" }, { "type": "null" } ], "description": "Default card on file. `null` when the account has never saved a payment method — the top-up flow will route through a checkout session in that case." } } }, "AccountPaymentMethodErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Human-readable error message.", "example": "Unauthorized" } } }, "SavedCard": { "type": "object", "required": [ "brand", "last4", "exp_month", "exp_year", "funding" ], "properties": { "brand": { "type": "string", "description": "Card brand. Common values: `visa`, `mastercard`, `amex`, `discover`, `diners`, `jcb`, `unionpay`, `unknown`.", "example": "visa" }, "last4": { "type": "string", "pattern": "^\\d{4}$", "description": "Last four digits of the card number. Safe to display in UI for identification.", "example": "4242" }, "exp_month": { "type": "integer", "minimum": 1, "maximum": 12, "description": "Card expiration month (1-12). Compare against the current month to detect expired cards.", "example": 12 }, "exp_year": { "type": "integer", "description": "Card expiration year (full 4-digit year). Compare against the current year to detect expired cards.", "example": 2026 }, "funding": { "type": "string", "description": "Funding type. Common values: `credit`, `debit`, `prepaid`, `unknown`.", "example": "credit" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Save a Payment Method Source: https://recoupable.dev/docs/api-reference/accounts/payment-method-create Create a $0 card-on-file checkout session for an account. Stripe `setup` mode saves a payment method without charging anything or starting a subscription; the saved card becomes the account's default and is what credit purchases and auto top-up charge. The card is saved on the account's Stripe customer that already holds a default card, or the newest tagged customer; a customer is created (and tagged with the account id) if none exists yet. Returns a hosted checkout URL that the client should redirect to. `id` may be the authenticated account or an organization the caller belongs to. ## POST /api/accounts/{id}/payment-method Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/accounts/{id}/payment-method": { "post": { "description": "Create a $0 card-on-file checkout session for an account. Stripe `setup` mode saves a payment method without charging anything or starting a subscription; the saved card becomes the account's default and is what credit purchases and auto top-up charge. The card is saved on the account's Stripe customer that already holds a default card, or the newest tagged customer; a customer is created (and tagged with the account id) if none exists yet. Returns a hosted checkout URL that the client should redirect to. `id` may be the authenticated account or an organization the caller belongs to.", "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier (UUID) of the account. Must be the authenticated account or another accessible via organization membership.", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Card-on-file session parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateCardOnFileSessionRequest" } } } }, "responses": { "200": { "description": "Card-on-file session created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSubscriptionSessionResponse" } } } }, "400": { "description": "Bad request - missing or invalid parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountPaymentMethodErrorResponse" }, "example": { "error": "successUrl must be a valid URL" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountPaymentMethodErrorResponse" }, "example": { "error": "Unauthorized" } } } }, "403": { "description": "Forbidden - the account is not the caller's and not an organization they belong to (an unknown id also returns 403, never 404)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountPaymentMethodErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountPaymentMethodErrorResponse" }, "example": { "error": "Internal server error" } } } } } } } }, "components": { "schemas": { "CreateCardOnFileSessionRequest": { "type": "object", "required": [ "successUrl" ], "properties": { "successUrl": { "type": "string", "format": "uri", "description": "The URL Stripe redirects to after the card is saved.", "example": "https://chat.recoupable.dev?card=saved" } } }, "CreateSubscriptionSessionResponse": { "type": "object", "required": [ "id", "url" ], "properties": { "id": { "type": "string", "description": "The checkout session ID.", "example": "cs_test_a1b2c3d4e5f6g7h8i9j0" }, "url": { "type": "string", "format": "uri", "description": "The hosted checkout URL. Redirect to this URL to complete Checkout.", "example": "https://checkout.stripe.com/pay/cs_test_a1b2c3d4e5f6g7h8i9j0" } } }, "AccountPaymentMethodErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Human-readable error message.", "example": "Unauthorized" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Remove the Payment Method Source: https://recoupable.dev/docs/api-reference/accounts/payment-method-delete Detach the default card on file from an account, whichever tagged Stripe customer holds it. After this call `GET /api/accounts/{id}/payment-method` returns `card: null`, credit purchases route through checkout again, and auto top-up (if enabled) is turned off because it has nothing to charge. Subscriptions collected by invoice are unaffected. `id` may be the authenticated account or an organization the caller belongs to. ## DELETE /api/accounts/{id}/payment-method Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/accounts/{id}/payment-method": { "delete": { "description": "Detach the default card on file from an account, whichever tagged Stripe customer holds it. After this call `GET /api/accounts/{id}/payment-method` returns `card: null`, credit purchases route through checkout again, and auto top-up (if enabled) is turned off because it has nothing to charge. Subscriptions collected by invoice are unaffected. `id` may be the authenticated account or an organization the caller belongs to.", "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier (UUID) of the account. Must be the authenticated account or another accessible via organization membership.", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "204": { "description": "Payment method detached; no body." }, "401": { "description": "Unauthorized - invalid or missing authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountPaymentMethodErrorResponse" }, "example": { "error": "Unauthorized" } } } }, "403": { "description": "Forbidden - the account is not the caller's and not an organization they belong to (an unknown id also returns 403, never 404)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountPaymentMethodErrorResponse" } } } } } } } }, "components": { "schemas": { "AccountPaymentMethodErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Human-readable error message.", "example": "Unauthorized" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # List Account Payments Source: https://recoupable.dev/docs/api-reference/accounts/payments-get List the invoices paid or owed by an account, newest first: subscription renewals, credit purchases, and invoiced enterprise plans alike. Invoices are merged across every Stripe customer tagged with the account id, so a card saved on one customer and an invoiced plan on another appear in one list. Non-draft rows carry the hosted invoice URL for the receipt; `url` is null for drafts. Returns an empty list (not 404) when the account has no tagged Stripe customer or no invoices yet. `startingAfter` is an invoice id from a previous page; the next page holds invoices created before it. `id` may be the authenticated account or an organization the caller belongs to. ## GET /api/accounts/{id}/payments Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/accounts/{id}/payments": { "get": { "description": "List the invoices paid or owed by an account, newest first: subscription renewals, credit purchases, and invoiced enterprise plans alike. Invoices are merged across every Stripe customer tagged with the account id, so a card saved on one customer and an invoiced plan on another appear in one list. Non-draft rows carry the hosted invoice URL for the receipt; `url` is null for drafts. Returns an empty list (not 404) when the account has no tagged Stripe customer or no invoices yet. `startingAfter` is an invoice id from a previous page; the next page holds invoices created before it. `id` may be the authenticated account or an organization the caller belongs to.", "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier (UUID) of the account. Must be the authenticated account or another accessible via organization membership.", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "limit", "in": "query", "description": "Maximum rows to return.", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 } }, { "name": "startingAfter", "in": "query", "description": "Cursor for the next page: the `id` of the last payment from the previous response.", "required": false, "schema": { "type": "string" }, "example": "in_1U5xj400JObOnOb5BE0CmxCt" } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Payments retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountPaymentsResponse" } } } }, "400": { "description": "Bad request - invalid `id`, `limit`, or `startingAfter`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountPaymentsErrorResponse" }, "example": { "error": "limit must be between 1 and 100" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountPaymentsErrorResponse" }, "example": { "error": "Unauthorized" } } } }, "403": { "description": "Forbidden - the account is not the caller's and not an organization they belong to (an unknown id also returns 403, never 404)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountPaymentsErrorResponse" } } } } } } } }, "components": { "schemas": { "AccountPaymentsResponse": { "type": "object", "required": [ "account_id", "payments", "hasMore" ], "properties": { "account_id": { "type": "string", "format": "uuid", "description": "The account these payments belong to.", "example": "550e8400-e29b-41d4-a716-446655440000" }, "payments": { "type": "array", "items": { "$ref": "#/components/schemas/AccountPayment" }, "description": "Invoices, newest first, merged across every Stripe customer tagged with the account id. Empty when the account has no tagged Stripe customer or no invoices." }, "hasMore": { "type": "boolean", "description": "True when another page exists; pass the last `id` as `startingAfter`.", "example": false } } }, "AccountPaymentsErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Human-readable error message.", "example": "Account not found" } } }, "AccountPayment": { "type": "object", "required": [ "id", "createdAt", "description", "amountCents", "currency", "status", "url" ], "properties": { "id": { "type": "string", "description": "Stripe invoice id; use as `startingAfter` to page.", "example": "in_1U5xj400JObOnOb5BE0CmxCt" }, "createdAt": { "type": "string", "format": "date-time", "description": "When the invoice was created.", "example": "2026-09-04T14:33:00Z" }, "description": { "type": "string", "description": "What was billed: the first line item's description, or the plan name.", "example": "Pro, monthly" }, "amountCents": { "type": "integer", "description": "Amount due in the smallest currency unit.", "example": 9900 }, "currency": { "type": "string", "description": "ISO 4217 currency code, lowercase.", "example": "usd" }, "status": { "type": "string", "enum": [ "draft", "open", "paid", "uncollectible", "void" ], "description": "Stripe invoice status.", "example": "paid" }, "url": { "type": "string", "format": "uri", "nullable": true, "description": "Hosted invoice page (receipt). Null for draft invoices.", "example": "https://invoice.stripe.com/i/acct_123/test_456" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Auto Top-up Settings Source: https://recoupable.dev/docs/api-reference/accounts/auto-top-up-get Read the auto top-up settings for an account. Auto top-up is opt-in: it is off for every account until the account turns it on and chooses both the amount to buy and the balance that triggers it. When it is on, the api charges the default card on file for `amountCents` the first time a credit deduction leaves the balance below `thresholdCents`, then grants the credits and emails a receipt. `id` may be the authenticated account or an organization the caller belongs to. ## GET /api/accounts/{id}/auto-top-up Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/accounts/{id}/auto-top-up": { "get": { "description": "Read the auto top-up settings for an account. Auto top-up is opt-in: it is off for every account until the account turns it on and chooses both the amount to buy and the balance that triggers it. When it is on, the api charges the default card on file for `amountCents` the first time a credit deduction leaves the balance below `thresholdCents`, then grants the credits and emails a receipt. `id` may be the authenticated account or an organization the caller belongs to.", "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier (UUID) of the account. Must be the authenticated account or another accessible via organization membership.", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Auto top-up settings (defaults when never configured).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AutoTopUpResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AutoTopUpErrorResponse" }, "example": { "error": "Unauthorized" } } } }, "403": { "description": "Forbidden - the account is not the caller's and not an organization they belong to (an unknown id also returns 403, never 404)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AutoTopUpErrorResponse" } } } } } } } }, "components": { "schemas": { "AutoTopUpResponse": { "type": "object", "required": [ "account_id", "enabled", "amountCents", "thresholdCents", "lastRunAt", "lastError" ], "properties": { "account_id": { "type": "string", "format": "uuid", "example": "550e8400-e29b-41d4-a716-446655440000" }, "enabled": { "type": "boolean", "description": "Whether auto top-up is on. Off by default.", "example": true }, "amountCents": { "type": "integer", "nullable": true, "description": "Amount charged and granted per top-up, in cents. Null until set.", "example": 10000 }, "thresholdCents": { "type": "integer", "nullable": true, "description": "Balance, in cents, below which a top-up runs. Null until set.", "example": 100 }, "lastRunAt": { "type": "string", "format": "date-time", "nullable": true, "description": "When the last auto top-up was attempted. Null until the first run.", "example": "2026-09-04T15:05:00Z" }, "lastError": { "type": "string", "nullable": true, "description": "Stripe decline message from the attempt that turned auto top-up off. Null while healthy or once re-enabled.", "example": "Your card was declined." } } }, "AutoTopUpErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Human-readable error message.", "example": "Add a payment method before turning on auto top-up" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Update Auto Top-up Settings Source: https://recoupable.dev/docs/api-reference/accounts/auto-top-up-update Set the auto top-up settings for an account. All three fields are required on every call; there is no partial update. Turning it on requires a card on file (`GET /api/accounts/{id}/payment-method` returns a card), an amount between 5.00 and 1,000.00 USD, and a threshold below the amount. Turning it off keeps the last amount and threshold so the account can turn it back on without retyping them. Guardrails that apply once it is on: at most one top-up per account per 10 minutes; a card decline turns `enabled` back to `false`, records `lastError`, and emails the account instead of retrying; removing the card turns it off. `id` may be the authenticated account or an organization the caller belongs to. ## PUT /api/accounts/{id}/auto-top-up Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/accounts/{id}/auto-top-up": { "put": { "description": "Set the auto top-up settings for an account. All three fields are required on every call; there is no partial update. Turning it on requires a card on file (`GET /api/accounts/{id}/payment-method` returns a card), an amount between 5.00 and 1,000.00 USD, and a threshold below the amount. Turning it off keeps the last amount and threshold so the account can turn it back on without retyping them. Guardrails that apply once it is on: at most one top-up per account per 10 minutes; a card decline turns `enabled` back to `false`, records `lastError`, and emails the account instead of retrying; removing the card turns it off. `id` may be the authenticated account or an organization the caller belongs to.", "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier (UUID) of the account. Must be the authenticated account or another accessible via organization membership.", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Auto top-up settings", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateAutoTopUpRequest" } } } }, "responses": { "200": { "description": "Settings saved.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AutoTopUpResponse" } } } }, "400": { "description": "Bad request - invalid body, `enabled: true` without a card on file, `amountCents` outside 500..100000, `thresholdCents` negative, or `thresholdCents` not below `amountCents`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AutoTopUpErrorResponse" }, "example": { "error": "Add a payment method before turning on auto top-up" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AutoTopUpErrorResponse" }, "example": { "error": "Unauthorized" } } } }, "403": { "description": "Forbidden - the account is not the caller's and not an organization they belong to (an unknown id also returns 403, never 404)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AutoTopUpErrorResponse" } } } }, "404": { "description": "The account has no credits row and one could not be created for it", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AutoTopUpErrorResponse" } } } } } } } }, "components": { "schemas": { "UpdateAutoTopUpRequest": { "type": "object", "required": [ "enabled", "amountCents", "thresholdCents" ], "properties": { "enabled": { "type": "boolean", "example": true }, "amountCents": { "type": "integer", "minimum": 500, "maximum": 100000, "description": "Amount to charge and grant per top-up, in cents (5.00 to 1,000.00 USD).", "example": 10000 }, "thresholdCents": { "type": "integer", "minimum": 0, "description": "Balance, in cents, below which a top-up runs. Must be below `amountCents`.", "example": 100 } } }, "AutoTopUpResponse": { "type": "object", "required": [ "account_id", "enabled", "amountCents", "thresholdCents", "lastRunAt", "lastError" ], "properties": { "account_id": { "type": "string", "format": "uuid", "example": "550e8400-e29b-41d4-a716-446655440000" }, "enabled": { "type": "boolean", "description": "Whether auto top-up is on. Off by default.", "example": true }, "amountCents": { "type": "integer", "nullable": true, "description": "Amount charged and granted per top-up, in cents. Null until set.", "example": 10000 }, "thresholdCents": { "type": "integer", "nullable": true, "description": "Balance, in cents, below which a top-up runs. Null until set.", "example": 100 }, "lastRunAt": { "type": "string", "format": "date-time", "nullable": true, "description": "When the last auto top-up was attempted. Null until the first run.", "example": "2026-09-04T15:05:00Z" }, "lastError": { "type": "string", "nullable": true, "description": "Stripe decline message from the attempt that turned auto top-up off. Null while healthy or once re-enabled.", "example": "Your card was declined." } } }, "AutoTopUpErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Human-readable error message.", "example": "Add a payment method before turning on auto top-up" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Create Credits Top-Up Session Source: https://recoupable.dev/docs/api-reference/credits/sessions-create Top up credits on the authenticated account. ## POST /api/credits/sessions Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/credits/sessions": { "post": { "description": "Top up credits on the authenticated account.\n\n**Credits are integer micro-dollars (1,000,000 = \\$1.00, the same 6-decimal unit as USDC).** The customer is charged the USD value of `credits` plus a Stripe processing fee (US card pricing: 2.9% + \\$0.30) — e.g. `credits: 100000000` charges \\$103.30 total (\\$100.00 credits + \\$3.30 fee).\n\n**Two outcomes, distinguished by response shape:**\n\n- **Auto-charged** — if the account has a card on file (from a prior subscription or top-up), the card is charged immediately and the response is `{ paymentIntentId, creditsPurchased, totalCents }`. Credits land in the account's balance asynchronously via Stripe webhook (typically within seconds). No human interaction required.\n\n- **Checkout required** — if no card is on file, or the saved card requires 3-D Secure authentication, the response is `{ id, url }` with a hosted Stripe Checkout URL. Redirect to that URL; credits land on successful payment.\n\nClients should discriminate on the presence of `url` (Checkout) vs `paymentIntentId` (auto-charged). Cards entered through the Checkout fallback are saved for future top-ups, so a customer's second top-up typically auto-charges.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Top-up parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateCreditsSessionRequest" } } } }, "responses": { "200": { "description": "Top-up initiated successfully. Some fields are conditional on which path was taken — see each field's description.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreditsTopupResponse" } } } }, "400": { "description": "Bad request - missing or invalid parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreditsSessionErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreditsSessionErrorResponse" } } } } } } } }, "components": { "schemas": { "CreateCreditsSessionRequest": { "type": "object", "required": [ "credits", "successUrl" ], "properties": { "successUrl": { "type": "string", "format": "uri", "description": "Required valid redirect URL, including when a saved card is charged directly. Used by Checkout fallback; no default is supplied.", "example": "https://chat.recoupable.dev?credits=success" }, "credits": { "type": "integer", "minimum": 10000, "description": "The number of credits to purchase, in integer micro-dollars (1,000,000 = \\$1.00), so the value in USD is `credits / 1000000`. Example: `2500000` is \\$2.50 worth of credits (the customer is also charged a Stripe processing fee on top).", "example": 2500000, "multipleOf": 10000 }, "accountId": { "type": "string", "format": "uuid", "description": "UUID of the account to create the session for. Only applicable when the authenticated account has admin access to multiple accounts. If not provided, creates a session for the API key's own account.", "example": "123e4567-e89b-12d3-a456-426614174000" } }, "additionalProperties": false, "example": { "credits": 2500000, "successUrl": "https://chat.recoupable.dev?credits=success" } }, "CreditsTopupResponse": { "type": "object", "description": "Top-up response. Two paths share this shape — clients can discriminate on the presence of `url` (Checkout fallback) vs `paymentIntentId` (auto-charged).", "properties": { "paymentIntentId": { "type": "string", "description": "**Auto-charge path only.** The Stripe PaymentIntent ID for the off-session charge. Useful for reconciliation and support. Omitted on the Checkout fallback path.", "example": "pi_3O1a2b3c4d5e6f7g_h8i9j0kL" }, "creditsPurchased": { "type": "integer", "minimum": 1, "description": "**Auto-charge path only.** The number of credits purchased, matching the `credits` request field. These credits are added to the account's balance asynchronously via Stripe webhook (typically within seconds). Omitted on the Checkout fallback path.", "example": 2500000 }, "totalCents": { "type": "integer", "minimum": 1, "description": "Auto-charge path only. Whole cents: creditsPurchased / 10000 plus the processing fee. Example: 2,500,000 micro-dollars is 250 cents before fees.", "example": 289 }, "id": { "type": "string", "description": "**Checkout fallback only.** The Stripe Checkout Session ID. Omitted on the auto-charge path.", "example": "cs_test_a1b2c3d4e5f6g7h8i9j0" }, "url": { "type": "string", "format": "uri", "description": "**Checkout fallback only.** The hosted Stripe Checkout URL — redirect the customer here to enter card details and complete payment. Omitted on the auto-charge path.", "example": "https://checkout.stripe.com/pay/cs_test_a1b2c3d4e5f6g7h8i9j0" } } }, "CreditsSessionErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Human-readable error message.", "example": "credits must be a positive integer" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Account Credits Source: https://recoupable.dev/docs/api-reference/accounts/credits-get Retrieve the current credit balance for an account. Returns the remaining credits along with the plan-derived monthly total and used count, plus a flag indicating whether the account is on a pro plan (directly or via an organization). Credits refill monthly; the `timestamp` field reflects the last refill or balance update. ## GET /api/accounts/{id}/credits Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/accounts/{id}/credits": { "get": { "description": "Retrieve the current credit balance for an account. Returns the remaining credits along with the plan-derived monthly total and used count, plus a flag indicating whether the account is on a pro plan (directly or via an organization). Credits refill monthly; the `timestamp` field reflects the last refill or balance update.", "parameters": [ { "name": "id", "in": "path", "description": "The unique identifier (UUID) of the account. Must be the authenticated account or another accessible via organization membership.", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Credit balance retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountCreditsResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountCreditsErrorResponse" }, "example": { "error": "Unauthorized" } } } }, "403": { "description": "Forbidden - account not accessible to the authenticated account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountCreditsErrorResponse" } } } } } } } }, "components": { "schemas": { "AccountCreditsResponse": { "type": "object", "required": [ "account_id", "remaining_credits", "total_credits", "used_credits", "is_pro", "timestamp", "plan", "task_limit", "min_cadence_minutes" ], "properties": { "account_id": { "type": "string", "format": "uuid", "description": "The unique identifier of the account this balance belongs to.", "example": "550e8400-e29b-41d4-a716-446655440000" }, "remaining_credits": { "type": "integer", "minimum": 0, "description": "Credits still available in the current monthly period. Refills automatically once the period elapses.", "example": 292450000 }, "total_credits": { "type": "integer", "minimum": 0, "description": "Monthly refill floor in integer micro-dollars: Free 3,330,000; Starter 20,000,000; Pro 300,000,000. Top-ups can put remaining credits above this floor.", "example": 300000000 }, "used_credits": { "type": "integer", "minimum": 0, "description": "max(0, total_credits - remaining_credits). A balance-derived display, not lifetime or period ledger spend.", "example": 7550000 }, "is_pro": { "type": "boolean", "description": "True when the account is on a pro plan, directly via an account subscription or via an organization subscription.", "example": false }, "timestamp": { "type": "string", "format": "date-time", "nullable": true, "description": "ISO 8601 timestamp of the last balance update or monthly refill. Null when the account has never spent or refilled credits.", "example": "2026-05-01T12:34:56.000Z" }, "plan": { "type": "string", "enum": [ "free", "starter", "pro" ] }, "task_limit": { "type": "integer", "nullable": true, "description": "Enabled scheduled-task limit; null means unlimited." }, "min_cadence_minutes": { "type": "integer", "description": "Minimum allowed cadence: Free 10080, Starter 1440, Pro 60." } } }, "AccountCreditsErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Human-readable error message.", "example": "Account not found" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Account Usage Source: https://recoupable.dev/docs/api-reference/accounts/usage-get List the charges that consumed an account's credits: one line item per deduction from `usage_events`, newest first, over a period. Each item carries the amount as the raw ledger integer (`credits_deducted`, micro-dollars: 1,000,000 = $1.00) and the same amount formatted as a dollar string (`usd`), and the response carries the total for the whole period, not just the page. Access is the same as [`GET /api/accounts/{id}/credits`](/docs/api-reference/accounts/credits-get): the authenticated account itself, or another account reachable through organization membership. ## GET /api/accounts/{id}/usage Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/accounts/{id}/usage": { "get": { "description": "List the charges that consumed an account's credits: one line item per deduction from `usage_events`, newest first, over a period. Each item carries the amount as the raw ledger integer (`credits_deducted`, micro-dollars: 1,000,000 = $1.00) and the same amount formatted as a dollar string (`usd`), and the response carries the total for the whole period, not just the page. Access is the same as [`GET /api/accounts/{id}/credits`](/docs/api-reference/accounts/credits-get): the authenticated account itself, or another account reachable through organization membership.", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The unique identifier (UUID) of the account. Must be the authenticated account or another accessible via organization membership.", "schema": { "type": "string", "format": "uuid" } }, { "name": "limit", "in": "query", "required": false, "description": "Maximum number of line items to return per page.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 } }, { "name": "sort", "in": "query", "required": false, "description": "Both orders are descending: created_at uses timestamp then row ID; cost uses credits_deducted then row ID. Cost ties are ordered by ID, not timestamp.", "schema": { "type": "string", "enum": [ "created_at", "cost" ], "default": "created_at" } }, { "name": "cursor", "in": "query", "required": false, "description": "Opaque paging token: pass next_cursor unchanged with the same sort, from and to. New time cursors retain both the last timestamp and row ID, so tied timestamps are not skipped. Cost cursors retain amount and row ID. Legacy timestamp-only cursors remain accepted as strict time boundaries; start a fresh traversal to obtain complete tie coverage.", "schema": { "type": "string" } }, { "name": "from", "in": "query", "required": false, "description": "Start of the period (inclusive), ISO 8601. Defaults to the start of the current UTC month.", "schema": { "type": "string", "format": "date-time" } }, { "name": "to", "in": "query", "required": false, "description": "End of the period (exclusive), ISO 8601. Defaults to now.", "schema": { "type": "string", "format": "date-time" } } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Usage line items retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountUsageResponse" } } } }, "400": { "description": "Bad request: invalid query parameter, limit above 100, malformed cursor, or a non-positive period after database timestamp normalization.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountCreditsErrorResponse" }, "example": { "error": "limit must be between 1 and 100" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountCreditsErrorResponse" }, "example": { "error": "Unauthorized" } } } }, "403": { "description": "Forbidden - account not accessible to the authenticated account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountCreditsErrorResponse" } } } } } } } }, "components": { "schemas": { "AccountUsageResponse": { "type": "object", "required": [ "account_id", "period", "total_credits_deducted", "total_usd", "events", "next_cursor" ], "properties": { "account_id": { "type": "string", "format": "uuid", "description": "The account whose charges are listed.", "example": "550e8400-e29b-41d4-a716-446655440000" }, "period": { "type": "object", "required": [ "from", "to" ], "description": "The period the totals cover.", "properties": { "from": { "type": "string", "format": "date-time", "example": "2026-08-01T00:00:00.000Z" }, "to": { "type": "string", "format": "date-time", "example": "2026-08-27T12:00:00.000Z" } } }, "total_credits_deducted": { "type": "integer", "description": "Sum of `credits_deducted` over every charge in the period, in micro-dollars. Covers the whole period, not only this page.", "example": 70000 }, "total_usd": { "type": "string", "description": "`total_credits_deducted` formatted as US dollars.", "example": "$0.07" }, "events": { "type": "array", "description": "Charges in the period in the requested `sort` order (newest first by default).", "items": { "$ref": "#/components/schemas/AccountUsageEvent" } }, "next_cursor": { "type": "string", "nullable": true, "description": "Opaque; pass unchanged as cursor with the same sort, from and to. A full page may return a cursor followed by an empty final page; null means this traversal has ended. Time cursors retain timestamp and ID; cost cursors retain amount and ID.", "example": "v1:WyIyMDI2LTA4LTI3VDExOjU2OjU4LjAwMDAwMFoiLCIzQUFObjNJajl1Ri16WklsV196bFAiXQ" }, "series_bucket": { "type": "string", "enum": [ "hour", "day", "week", "month" ], "description": "Granularity of `series`, derived from the span of the period: `hour` up to 2 days, `day` up to 90 days, `week` up to 12 months, `month` beyond. Present only on a first page (no `cursor`).", "example": "day" }, "series": { "type": "array", "description": "Spend over the period, one entry per `series_bucket` that had at least one charge, ascending by `start`, in UTC. The sum of `credits_deducted` across the entries equals `total_credits_deducted`. Present only on a first page (no `cursor`); omitted on cursor pages so paging never recomputes it.", "items": { "type": "object", "required": [ "start", "credits_deducted", "usd", "events" ], "properties": { "start": { "type": "string", "format": "date-time", "description": "Start of the bucket, UTC.", "example": "2026-08-12T00:00:00.000Z" }, "credits_deducted": { "type": "integer", "description": "Charges in the bucket, in micro-dollars.", "example": 221090000 }, "usd": { "type": "string", "description": "`credits_deducted` formatted as US dollars.", "example": "$221.09" }, "events": { "type": "integer", "description": "Number of charges in the bucket.", "example": 2462 } } }, "example": [ { "start": "2026-08-11T00:00:00.000Z", "credits_deducted": 56080000, "usd": "$56.08", "events": 727 }, { "start": "2026-08-12T00:00:00.000Z", "credits_deducted": 221090000, "usd": "$221.09", "events": 2462 }, { "start": "2026-08-13T00:00:00.000Z", "credits_deducted": 440930000, "usd": "$440.93", "events": 5479 } ] } } }, "AccountCreditsErrorResponse": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "description": "Human-readable error message.", "example": "Account not found" } } }, "AccountUsageEvent": { "type": "object", "required": [ "id", "created_at", "source", "agent_type", "provider", "model_id", "input_tokens", "cached_input_tokens", "output_tokens", "tool_call_count", "credits_deducted", "usd", "resource_url" ], "properties": { "id": { "type": "string", "description": "Identifier of the `usage_events` row.", "example": "3AANn3Ij9uF-zZIlW_zlP" }, "created_at": { "type": "string", "format": "date-time", "description": "When the charge was recorded, normalized to UTC at database microsecond precision.", "example": "2026-08-27T11:56:58.000000Z" }, "source": { "type": "string", "description": "Which surface originated the charge: `api` for a request to the API, `web` for the chat app. Null when this field was not recorded.", "example": "api", "nullable": true }, "agent_type": { "type": "string", "description": "`main` for a top-level request or any non-agent charge; `subagent` for a nested task step. Null when this field was not recorded.", "example": "main", "nullable": true }, "provider": { "type": "string", "nullable": true, "description": "Provider slug when the charge is a model or generation call (for example `anthropic`, `fal`). Null for research and other fixed-price endpoints.", "example": "fal" }, "model_id": { "type": "string", "nullable": true, "description": "The model for LLM-backed charges (a chat turn, a song generation), otherwise the API endpoint that was billed, as `METHOD /route/pattern` (for example `POST /api/artist/socials/scrape`). Null on rows written before 2026-08-27.", "example": "POST /api/artist/socials/scrape" }, "input_tokens": { "type": "integer", "description": "Input tokens for the call, including cached. 0 for non-LLM charges. Null when this field was not recorded.", "example": 0, "nullable": true }, "cached_input_tokens": { "type": "integer", "description": "Subset of `input_tokens` served from the provider cache. Null when this field was not recorded.", "example": 0, "nullable": true }, "output_tokens": { "type": "integer", "description": "Output tokens for the call. 0 for non-LLM charges. Null when this field was not recorded.", "example": 0, "nullable": true }, "tool_call_count": { "type": "integer", "description": "Number of tool calls in this step. Null when this field was not recorded.", "example": 0, "nullable": true }, "credits_deducted": { "type": "integer", "description": "Amount charged, as the raw ledger integer in micro-dollars (1,000,000 = $1.00). Divide by 1,000,000 for dollars.", "example": 20000 }, "usd": { "type": "string", "description": "`credits_deducted` formatted as US dollars with two decimals.", "example": "$0.02" }, "resource_url": { "type": "string", "nullable": true, "description": "App-relative path of what produced the charge, when there is something to open: `/chat?roomId=` for a chat turn, `/music/` for a song, `/tasks//runs/` for a scheduled task run. Null for plain API calls and for rows written before the field existed.", "example": "/music/0c35429f-deb8-48f0-b0f2-fd5145de2583" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Update Pulse Source: https://recoupable.dev/docs/api-reference/pulses/update Update the pulse settings for an account. Use this to enable or disable the pulse. Returns an array of pulses for consistency with the GET endpoint. ## PATCH /api/pulses Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/pulses": { "patch": { "description": "Update the pulse settings for an account. Use this to enable or disable the pulse. Returns an array of pulses for consistency with the GET endpoint.", "requestBody": { "description": "Pulse fields to update", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdatePulseRequest" } } } }, "responses": { "200": { "description": "Pulse updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PulsesResponse" } } } }, "400": { "description": "Bad request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized - invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "UpdatePulseRequest": { "type": "object", "required": [ "active" ], "properties": { "active": { "type": "boolean", "description": "Whether to enable or disable the pulse", "example": true }, "account_id": { "type": "string", "format": "uuid", "description": "UUID of the account to update the pulse for. Only applicable when the authenticated account has access to multiple accounts via organization membership. If not provided, updates the pulse for the API key's own account." } } }, "PulsesResponse": { "type": "object", "required": [ "status", "pulses" ], "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "pulses": { "type": "array", "items": { "$ref": "#/components/schemas/Pulse" }, "description": "Array of pulse objects. Contains pulses for the authenticated account. If the account has access to organizations, all organization accounts are included." }, "error": { "type": "string", "description": "Error message (only present if status is error)" } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } }, "Pulse": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the pulse" }, "account_id": { "type": "string", "format": "uuid", "description": "Unique identifier for the associated account" }, "active": { "type": "boolean", "description": "Whether the pulse is active (defaults to true)" } } } } } } ``` # List Pulses Source: https://recoupable.dev/docs/api-reference/pulses/list Retrieve pulse information for the authenticated account. If the account has access to organizations, pass account_id to filter to a specific account within those organizations. ## GET /api/pulses Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/pulses": { "get": { "description": "Retrieve pulse information for the authenticated account. If the account has access to organizations, pass account_id to filter to a specific account within those organizations.", "parameters": [ { "name": "account_id", "in": "query", "description": "Filter to a specific account. Only applicable when the authenticated account has access to multiple accounts via organization membership.", "required": false, "schema": { "type": "string", "format": "uuid" } }, { "name": "active", "in": "query", "description": "Filter by active status. Set to 'true' to return only active pulses, 'false' for inactive pulses. If not provided, returns all pulses regardless of active status.", "required": false, "schema": { "type": "string", "enum": [ "true", "false" ] } } ], "responses": { "200": { "description": "Pulses retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PulsesResponse" } } } }, "400": { "description": "Bad request - invalid query parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized - invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden - account_id is not a member of the organization or account tried to filter by an account_id they don't have access to", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "PulsesResponse": { "type": "object", "required": [ "status", "pulses" ], "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "pulses": { "type": "array", "items": { "$ref": "#/components/schemas/Pulse" }, "description": "Array of pulse objects. Contains pulses for the authenticated account. If the account has access to organizations, all organization accounts are included." }, "error": { "type": "string", "description": "Error message (only present if status is error)" } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } }, "Pulse": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the pulse" }, "account_id": { "type": "string", "format": "uuid", "description": "Unique identifier for the associated account" }, "active": { "type": "boolean", "description": "Whether the pulse is active (defaults to true)" } } } } } } ``` # Get Organizations Source: https://recoupable.dev/docs/api-reference/organizations/list Retrieve all organizations that the authenticated account belongs to. Pass account_id to retrieve organizations for a specific account the API key has access to. ## GET /api/organizations Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/organizations": { "get": { "description": "Retrieve all organizations that the authenticated account belongs to. Pass account_id to retrieve organizations for a specific account the API key has access to.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "account_id", "in": "query", "description": "Filter to a specific account. Only applicable when the authenticated account has access to multiple accounts via organization membership.", "required": false, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Organizations retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GetOrganizationsResponse" } } } }, "400": { "description": "Bad request - invalid query parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OrganizationsErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden - account_id is not a member of the organization or account tried to filter by an account_id they don't have access to", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "GetOrganizationsResponse": { "type": "object", "required": [ "status", "organizations" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "organizations": { "type": "array", "items": { "$ref": "#/components/schemas/Organization" }, "description": "List of organizations the account belongs to" } } }, "OrganizationsErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } }, "Organization": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUID of the membership record" }, "organization_id": { "type": "string", "format": "uuid", "description": "UUID of the organization account" }, "organization_name": { "type": "string", "description": "Display name of the organization" }, "organization_image": { "type": "string", "nullable": true, "description": "Organization logo/image URL" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Create Organization Source: https://recoupable.dev/docs/api-reference/organizations/create Create a new organization. The creator is automatically added as a member of the organization. ## POST /api/organizations Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/organizations": { "post": { "description": "Create a new organization. The creator is automatically added as a member of the organization.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Organization creation parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateOrganizationRequest" } } } }, "responses": { "200": { "description": "Organization created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateOrganizationResponse" } } } }, "400": { "description": "Bad request - missing required parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OrganizationsErrorResponse" } } } } } } } }, "components": { "schemas": { "CreateOrganizationRequest": { "type": "object", "required": [ "name", "accountId" ], "properties": { "name": { "type": "string", "description": "The name of the organization to create", "example": "My New Label" }, "accountId": { "type": "string", "format": "uuid", "description": "The account ID of the creator", "example": "123e4567-e89b-12d3-a456-426614174000" } } }, "CreateOrganizationResponse": { "type": "object", "required": [ "status", "organization" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "organization": { "$ref": "#/components/schemas/CreatedOrganization", "description": "The created organization" } } }, "OrganizationsErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } }, "CreatedOrganization": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUID of the new organization account" }, "name": { "type": "string", "description": "Name of the organization" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Add Artist to Organization Source: https://recoupable.dev/docs/api-reference/organizations/add-artist Add an artist to an organization. This allows organization members to access and manage the artist. This endpoint is idempotent - calling it multiple times with the same artistId and organizationId will not create duplicate records. ## POST /api/organizations/artists Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/organizations/artists": { "post": { "description": "Add an artist to an organization. This allows organization members to access and manage the artist. This endpoint is idempotent - calling it multiple times with the same artistId and organizationId will not create duplicate records.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Artist-organization association parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AddArtistToOrganizationRequest" } } } }, "responses": { "200": { "description": "Artist added to organization successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AddArtistToOrganizationResponse" } } } }, "400": { "description": "Bad request - missing required parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OrganizationsErrorResponse" } } } }, "401": { "description": "Missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OrganizationsErrorResponse" } } } }, "403": { "description": "Caller is not a member of the organization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OrganizationsErrorResponse" } } } } } } } }, "components": { "schemas": { "AddArtistToOrganizationRequest": { "type": "object", "required": [ "artistId", "organizationId" ], "properties": { "artistId": { "type": "string", "format": "uuid", "description": "The account ID of the artist to add", "example": "artist-account-uuid" }, "organizationId": { "type": "string", "format": "uuid", "description": "The account ID of the organization", "example": "org-account-uuid" } } }, "AddArtistToOrganizationResponse": { "type": "object", "required": [ "status", "id" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "id": { "type": "string", "format": "uuid", "description": "UUID of the created artist-organization link" } } }, "OrganizationsErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Add Member to Organization Source: https://recoupable.dev/docs/api-reference/organizations/add-member Add a member to an organization. This endpoint is idempotent - Adding an existing member returns the existing membership. ## POST /api/organizations/members Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/organizations/members": { "post": { "description": "Add a member to an organization. This endpoint is idempotent - Adding an existing member returns the existing membership.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Member-organization association parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AddOrganizationMemberRequest" } } } }, "responses": { "200": { "description": "Member added to organization successfully (or already a member)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AddOrganizationMemberResponse" } } } }, "400": { "description": "Bad request - invalid or missing parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, "401": { "description": "Unauthorized - missing or invalid authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, "403": { "description": "Forbidden - caller is not a member of the organization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } } } } } }, "components": { "schemas": { "AddOrganizationMemberRequest": { "type": "object", "required": [ "organizationId" ], "properties": { "organizationId": { "type": "string", "format": "uuid", "description": "The account ID of the organization" }, "accountId": { "type": "string", "format": "uuid", "description": "The account ID of the member to add. Provide exactly one of accountId or email." }, "email": { "type": "string", "format": "email", "description": "Email of the member to add. The account is created if it does not exist yet. Provide exactly one of accountId or email." } } }, "AddOrganizationMemberResponse": { "type": "object", "required": [ "status", "id", "account_id" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "id": { "type": "string", "format": "uuid", "description": "UUID of the membership record" }, "account_id": { "type": "string", "format": "uuid", "description": "The account ID of the member (resolved from email when email was provided)" } } }, "ErrorEnvelope": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Remove Member from Organization Source: https://recoupable.dev/docs/api-reference/organizations/remove-member Remove a member from an organization. This endpoint is idempotent - Removing an account that is not a member succeeds without error. ## DELETE /api/organizations/members Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/organizations/members": { "delete": { "description": "Remove a member from an organization. This endpoint is idempotent - Removing an account that is not a member succeeds without error.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "organization_id", "in": "query", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The account ID of the organization" }, { "name": "account_id", "in": "query", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The account ID of the member to remove" } ], "responses": { "200": { "description": "Member removed from organization (or was not a member)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RemoveOrganizationMemberResponse" } } } }, "400": { "description": "Bad request - invalid or missing parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, "401": { "description": "Unauthorized - missing or invalid authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, "403": { "description": "Forbidden - caller is not a member of the organization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } } } } } }, "components": { "schemas": { "RemoveOrganizationMemberResponse": { "type": "object", "required": [ "status" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" } } }, "ErrorEnvelope": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # List Organization Domains Source: https://recoupable.dev/docs/api-reference/organizations/list-domains List the email domains mapped to an organization. Accounts that sign up with an email at a mapped domain automatically join the organization. ## GET /api/organizations/domains Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/organizations/domains": { "get": { "description": "List the email domains mapped to an organization. Accounts that sign up with an email at a mapped domain automatically join the organization.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "organization_id", "in": "query", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The account ID of the organization" } ], "responses": { "200": { "description": "Domains retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GetOrganizationDomainsResponse" } } } }, "400": { "description": "Bad request - invalid or missing parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, "401": { "description": "Unauthorized - missing or invalid authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, "403": { "description": "Forbidden - caller is not a member of the organization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } } } } } }, "components": { "schemas": { "GetOrganizationDomainsResponse": { "type": "object", "required": [ "status", "domains" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "domains": { "type": "array", "items": { "$ref": "#/components/schemas/OrganizationDomain" }, "description": "Domains mapped to the organization" } } }, "ErrorEnvelope": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "OrganizationDomain": { "type": "object", "required": [ "id", "domain", "organization_id" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "UUID of the domain mapping record" }, "domain": { "type": "string", "description": "The email domain (lowercase, no @)", "example": "seekermusic.com" }, "organization_id": { "type": "string", "format": "uuid", "description": "The account ID of the organization" }, "created_at": { "type": "string", "format": "date-time", "description": "When the mapping was created" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Add Organization Domain Source: https://recoupable.dev/docs/api-reference/organizations/add-domain Map an email domain to an organization for automatic membership. After mapping, any account that signs up (or signs in) with an email at this domain is automatically added to the organization. A domain can belong to at most one organization. This endpoint is idempotent - re-adding the same domain to the same organization returns the existing mapping. ## POST /api/organizations/domains Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/organizations/domains": { "post": { "description": "Map an email domain to an organization for automatic membership. After mapping, any account that signs up (or signs in) with an email at this domain is automatically added to the organization. A domain can belong to at most one organization. This endpoint is idempotent - re-adding the same domain to the same organization returns the existing mapping.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Domain-organization mapping parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AddOrganizationDomainRequest" } } } }, "responses": { "200": { "description": "Domain mapped to organization successfully (or already mapped)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AddOrganizationDomainResponse" } } } }, "400": { "description": "Bad request - invalid or missing parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, "401": { "description": "Unauthorized - missing or invalid authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, "403": { "description": "Forbidden - caller is not a member of the organization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, "409": { "description": "Conflict - domain is already mapped to a different organization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } } } } } }, "components": { "schemas": { "AddOrganizationDomainRequest": { "type": "object", "required": [ "organizationId", "domain" ], "properties": { "organizationId": { "type": "string", "format": "uuid", "description": "The account ID of the organization" }, "domain": { "type": "string", "description": "The email domain to map (lowercase, no @)", "example": "seekermusic.com" } } }, "AddOrganizationDomainResponse": { "type": "object", "required": [ "status", "id", "domain", "organization_id" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "id": { "type": "string", "format": "uuid", "description": "UUID of the domain mapping record" }, "domain": { "type": "string", "description": "The mapped email domain", "example": "seekermusic.com" }, "organization_id": { "type": "string", "format": "uuid", "description": "The account ID of the organization" } } }, "ErrorEnvelope": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Remove Organization Domain Source: https://recoupable.dev/docs/api-reference/organizations/remove-domain Remove an email domain mapping from an organization. New signups at this domain will no longer auto-join the organization; existing members are not affected. This endpoint is idempotent. ## DELETE /api/organizations/domains Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/organizations/domains": { "delete": { "description": "Remove an email domain mapping from an organization. New signups at this domain will no longer auto-join the organization; existing members are not affected. This endpoint is idempotent.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "organization_id", "in": "query", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The account ID of the organization" }, { "name": "domain", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The email domain to unmap (e.g. \"seekermusic.com\")" } ], "responses": { "200": { "description": "Domain mapping removed (or did not exist)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RemoveOrganizationDomainResponse" } } } }, "400": { "description": "Bad request - invalid or missing parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, "401": { "description": "Unauthorized - missing or invalid authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, "403": { "description": "Forbidden - caller is not a member of the organization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } } } } } }, "components": { "schemas": { "RemoveOrganizationDomainResponse": { "type": "object", "required": [ "status" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" } } }, "ErrorEnvelope": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # List Professional Roster Source: https://recoupable.dev/docs/api-reference/organizations/professionals List professional roster ## GET /api/organizations/professionals Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/organizations/professionals": { "get": { "summary": "List professional roster", "description": "List organization-private songwriter/producer records. Names are discovery candidates, never identity proof. Follow next_cursor until null to inspect the complete roster. Membership is rechecked on every page; responses are private and not cached. Legacy MCP equivalent (API key or Privy token): list_professional_roster. Not exposed through delegated OAuth MCP.", "security": [ { "bearerAuth": [] }, { "apiKeyAuth": [] } ], "parameters": [ { "name": "organization_id", "in": "query", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "after", "in": "query", "required": false, "schema": { "type": "string", "format": "uuid" }, "description": "next_cursor returned by the previous page." } ], "responses": { "200": { "description": "Up to 100 professionals", "content": { "application/json": { "schema": { "type": "object", "required": [ "professionals", "next_cursor" ], "properties": { "professionals": { "type": "array", "items": { "$ref": "#/components/schemas/OrganizationProfessional" } }, "next_cursor": { "type": [ "string", "null" ], "format": "uuid" } } } } } }, "400": { "description": "Invalid organization ID or cursor", "content": { "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "string" } } } } } }, "401": { "description": "Authentication required", "content": { "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "string" } } } } } }, "403": { "description": "Organization access denied, revoked, or record outside this organization", "content": { "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "string" } } } } } }, "503": { "description": "Unavailable storage or invalid response; retry the same page", "content": { "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "string" } } } } } } } } } }, "components": { "schemas": { "OrganizationProfessional": { "type": "object", "required": [ "id", "organization_id", "name", "roles", "confirmed_by", "confirmation_basis", "created_at", "updated_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "organization_id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "roles": { "type": "array", "items": { "type": "string", "enum": [ "songwriter", "producer" ] } }, "confirmed_by": { "type": "string", "format": "uuid" }, "confirmation_basis": { "type": "string", "enum": [ "operator_confirmed" ], "description": "Operator assertion for this organization; not a globally verified person identity." }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" } } } }, "securitySchemes": { "bearerAuth": { "type": "http", "scheme": "bearer" }, "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." } } } } ``` # Confirm Professional Roster Addition Source: https://recoupable.dev/docs/api-reference/organizations/professionals-confirm Confirm professional roster addition ## POST /api/organizations/professionals Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/organizations/professionals": { "post": { "summary": "Confirm professional roster addition", "description": "Requires explicit operator identity confirmation and organization roster intent. Use new only after confirming a distinct professional, including resolving same-name candidates; never infer identity from a name. Use existing with a professional_id from this organization to add roles without renaming. Both songwriter and producer can belong to one record. This operation creates no login account, shared canonical identity, catalog rights, enrichment job, or cross-workspace context. Research/name intake alone never calls this operation. Membership is rechecked before idempotency replay. Legacy MCP equivalent (API key or Privy token): confirm_professional_roster. Not exposed through delegated OAuth MCP. Returns the original saved response on retry, including its created flag and original status (201 for a new record, 200 for an existing record).", "security": [ { "bearerAuth": [] }, { "apiKeyAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": false, "properties": { "organization_id": { "type": "string", "format": "uuid" }, "idempotency_key": { "type": "string", "format": "uuid", "description": "Generate once per deliberate operation. Persist and reuse after a timeout or lost response. Scoped to the organization and actor. Different input with the same key is rejected." }, "mode": { "type": "string", "enum": [ "new", "existing" ] }, "name": { "type": "string", "minLength": 2, "maxLength": 200 }, "professional_id": { "type": "string", "format": "uuid" }, "roles": { "type": "array", "minItems": 1, "maxItems": 2, "items": { "type": "string", "enum": [ "songwriter", "producer" ] } }, "confirmed": { "type": "boolean", "enum": [ true ] }, "roster_intent": { "type": "string", "enum": [ "add" ] } }, "required": [ "organization_id", "idempotency_key", "mode", "roles", "confirmed", "roster_intent" ], "oneOf": [ { "properties": { "mode": { "enum": [ "new" ] } }, "required": [ "name" ], "not": { "required": [ "professional_id" ] } }, { "properties": { "mode": { "enum": [ "existing" ] } }, "required": [ "professional_id" ], "not": { "required": [ "name" ] } } ] } } } }, "responses": { "200": { "description": "Existing professional roles saved, or replayed existing-record operation", "content": { "application/json": { "schema": { "type": "object", "required": [ "professional", "created" ], "properties": { "professional": { "$ref": "#/components/schemas/OrganizationProfessional" }, "created": { "type": "boolean" } } } } } }, "201": { "description": "New professional saved, or replayed new-record operation", "content": { "application/json": { "schema": { "type": "object", "required": [ "professional", "created" ], "properties": { "professional": { "$ref": "#/components/schemas/OrganizationProfessional" }, "created": { "type": "boolean" } } } } } }, "400": { "description": "Invalid or unconfirmed request", "content": { "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "string" } } } } } }, "401": { "description": "Authentication required", "content": { "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "string" } } } } } }, "403": { "description": "Organization access denied, revoked, or record outside this organization", "content": { "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "string" } } } } } }, "409": { "description": "Request key already used for different input or actor", "content": { "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "string" } } } } } }, "503": { "description": "Unavailable storage or unconfirmed response; retry with the same key and input", "content": { "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "string" } } } } } } } } } }, "components": { "schemas": { "OrganizationProfessional": { "type": "object", "required": [ "id", "organization_id", "name", "roles", "confirmed_by", "confirmation_basis", "created_at", "updated_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "organization_id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "roles": { "type": "array", "items": { "type": "string", "enum": [ "songwriter", "producer" ] } }, "confirmed_by": { "type": "string", "format": "uuid" }, "confirmation_basis": { "type": "string", "enum": [ "operator_confirmed" ], "description": "Operator assertion for this organization; not a globally verified person identity." }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" } } } }, "securitySchemes": { "bearerAuth": { "type": "http", "scheme": "bearer" }, "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." } } } } ``` # Check Admin Status Source: https://recoupable.dev/docs/api-reference/admins/check Check if the authenticated account is a Recoup admin. An account is considered an admin if it is a member of the Recoup organization. No input parameters required — authentication is performed via the x-api-key or Authorization header. ## GET /api/admins Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/admins": { "get": { "description": "Check if the authenticated account is a Recoup admin. An account is considered an admin if it is a member of the Recoup organization. No input parameters required — authentication is performed via the x-api-key or Authorization header.", "parameters": [], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Admin status retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CheckAdminResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } } } } } }, "components": { "schemas": { "CheckAdminResponse": { "type": "object", "required": [ "status", "isAdmin" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "isAdmin": { "type": "boolean", "description": "Whether the authenticated account is a Recoup admin" } } }, "AccountErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # List Account Sandboxes (Admin) Source: https://recoupable.dev/docs/api-reference/admins/sandboxes Returns a list of all accounts and their sandbox usage statistics. Each item includes the account email, total number of sandboxes created, and the timestamp of the most recently created sandbox. Requires the authenticated account to be a Recoup admin. Authentication via x-api-key or Authorization Bearer token. ## GET /api/admins/sandboxes Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/admins/sandboxes": { "get": { "description": "Returns a list of all accounts and their sandbox usage statistics. Each item includes the account email, total number of sandboxes created, and the timestamp of the most recently created sandbox. Requires the authenticated account to be a Recoup admin. Authentication via x-api-key or Authorization Bearer token.", "parameters": [], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Account sandbox statistics retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AdminSandboxesResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "403": { "description": "Forbidden - authenticated account is not a Recoup admin", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } } } } } }, "components": { "schemas": { "AdminSandboxesResponse": { "type": "object", "required": [ "status", "accounts" ], "description": "Response containing per-account sandbox statistics for admin use", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "accounts": { "type": "array", "description": "List of accounts with their sandbox statistics, ordered by most recently active first", "items": { "$ref": "#/components/schemas/AccountSandboxRow" } } } }, "AccountErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } }, "AccountSandboxRow": { "type": "object", "required": [ "account_id", "total_sandboxes", "last_created_at" ], "properties": { "account_id": { "type": "string", "format": "uuid", "description": "The unique identifier of the account", "example": "04e3aba9-c130-4fb8-8b92-34e95d43e66b" }, "total_sandboxes": { "type": "integer", "description": "Total number of sandboxes created for this account", "example": 5 }, "last_created_at": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp of the most recently created sandbox for this account", "example": "2026-03-10T12:00:00Z" }, "account_email": { "type": "string", "nullable": true, "description": "The email address of the account. Null if the account has no email set.", "example": "alice@example.com" } }, "description": "" } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # List Org Repo Stats (Admin) Source: https://recoupable.dev/docs/api-reference/admins/sandboxes-orgs Returns commit statistics for each repository in the recoupable GitHub organization. Each item includes the repo name, URL, total commit count, latest 5 commit messages, earliest and latest commit timestamps, and the list of account repo URLs that include this org repo as a submodule. Requires the authenticated account to be a Recoup admin. Authentication via x-api-key or Authorization Bearer token. ## GET /api/admins/sandboxes/orgs Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/admins/sandboxes/orgs": { "get": { "description": "Returns commit statistics for each repository in the recoupable GitHub organization. Each item includes the repo name, URL, total commit count, latest 5 commit messages, earliest and latest commit timestamps, and the list of account repo URLs that include this org repo as a submodule. Requires the authenticated account to be a Recoup admin. Authentication via x-api-key or Authorization Bearer token.", "parameters": [], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Org repo statistics retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AdminSandboxOrgsResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "403": { "description": "Forbidden - authenticated account is not a Recoup admin", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } } } } } }, "components": { "schemas": { "AdminSandboxOrgsResponse": { "type": "object", "required": [ "status", "repos" ], "description": "Response containing commit statistics for all org repositories", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "repos": { "type": "array", "description": "List of org repos with their commit statistics, ordered by total_commits descending", "items": { "$ref": "#/components/schemas/OrgRepoRow" } } } }, "AccountErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } }, "OrgRepoRow": { "type": "object", "required": [ "repo_name", "repo_url", "total_commits", "latest_commit_messages", "earliest_committed_at", "latest_committed_at", "account_repos" ], "description": "Commit statistics for a single GitHub org repository", "properties": { "repo_name": { "type": "string", "description": "Repository name", "example": "chat" }, "repo_url": { "type": "string", "description": "Full GitHub HTML URL of the repository", "example": "https://github.com/recoupable/chat" }, "total_commits": { "type": "integer", "description": "Total number of commits in the repository", "example": 5696 }, "latest_commit_messages": { "type": "array", "description": "Messages from the 5 most recent commits", "items": { "type": "string" }, "example": [ "Merge test into main", "fix: duration stuck at 0ms for in-progress tasks" ] }, "earliest_committed_at": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp of the earliest (first) commit", "example": "2024-09-27T17:16:01Z" }, "latest_committed_at": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp of the most recent commit", "example": "2026-03-10T19:57:37Z" }, "account_repos": { "type": "array", "items": { "type": "object", "properties": { "account_id": { "type": "string" }, "email": { "type": "string", "nullable": true }, "repo_url": { "type": "string" } }, "required": [ "account_id", "repo_url" ] }, "description": "List of accounts using this org repo as a submodule, with account_id, email, and repo_url" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # List Account Emails (Admin) Source: https://recoupable.dev/docs/api-reference/admins/emails Returns Resend emails including HTML content. Provide either account_id (returns all emails for an account) or email_id (returns a single email by Resend ID). Requires the authenticated account to be a Recoup admin. Authentication via x-api-key or Authorization Bearer token. Email response shape matches the [Resend Retrieve Email API](https://resend.com/docs/api-reference/emails/retrieve-email). ## GET /api/admins/emails Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/admins/emails": { "get": { "description": "Returns Resend emails including HTML content. Provide either account_id (returns all emails for an account) or email_id (returns a single email by Resend ID). Requires the authenticated account to be a Recoup admin. Authentication via x-api-key or Authorization Bearer token. Email response shape matches the [Resend Retrieve Email API](https://resend.com/docs/api-reference/emails/retrieve-email).", "parameters": [ { "name": "account_id", "in": "query", "description": "The account ID to fetch all emails for. Required if email_id is not provided.", "required": false, "schema": { "type": "string", "format": "uuid" } }, { "name": "email_id", "in": "query", "description": "A Resend email ID to fetch a single email. Required if account_id is not provided.", "required": false, "schema": { "type": "string" } } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Emails retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AdminEmailsResponse" } } } }, "400": { "description": "Bad request - must provide either account_id or email_id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "403": { "description": "Forbidden - authenticated account is not a Recoup admin", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } } } } } }, "components": { "schemas": { "AdminEmailsResponse": { "type": "object", "required": [ "status", "emails" ], "description": "Response containing Resend emails sent for an account", "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "emails": { "type": "array", "description": "List of emails sent for the account, ordered by created_at descending", "items": { "$ref": "#/components/schemas/PulseEmailRow" } } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } }, "AccountErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } }, "PulseEmailRow": { "type": "object", "required": [ "id", "from", "to", "subject", "created_at", "last_event" ], "description": "A single email record from Resend (full GetEmailResponseSuccess). See [Resend API docs](https://resend.com/docs/api-reference/emails/retrieve-email) for the source of truth.", "properties": { "id": { "type": "string", "description": "The Resend email ID" }, "from": { "type": "string", "description": "Sender email address" }, "to": { "type": "array", "items": { "type": "string" }, "description": "Recipient email addresses" }, "cc": { "type": "array", "items": { "type": "string" }, "nullable": true, "description": "CC recipient email addresses" }, "bcc": { "type": "array", "items": { "type": "string" }, "nullable": true, "description": "BCC recipient email addresses" }, "reply_to": { "type": "array", "items": { "type": "string" }, "nullable": true, "description": "Reply-to email addresses" }, "subject": { "type": "string", "description": "Email subject line" }, "html": { "type": "string", "nullable": true, "description": "HTML content of the email" }, "text": { "type": "string", "nullable": true, "description": "Plain text content of the email" }, "created_at": { "type": "string", "format": "date-time", "description": "Timestamp when the email was created" }, "scheduled_at": { "type": "string", "format": "date-time", "nullable": true, "description": "Scheduled send time, if any" }, "last_event": { "type": "string", "enum": [ "bounced", "canceled", "clicked", "complained", "delivered", "delivery_delayed", "failed", "opened", "queued", "scheduled", "sent" ], "description": "Most recent delivery event for this email" }, "tags": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "value": { "type": "string" } }, "required": [ "name", "value" ] }, "nullable": true, "description": "Custom tags attached to the email" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # List Privy Logins (Admin) Source: https://recoupable.dev/docs/api-reference/admins/privy Returns Privy login statistics for a given time period. Results include counts for new accounts (created_at), active accounts (latest_verified_at), total Privy accounts across all time, and the full, unmodified Privy account objects. See [Privy User object documentation](https://docs.privy.io/api-reference/users/get-all) for the complete type definition. Defaults to period=all (no date filter). Requires the authenticated account to be a Recoup admin. Authentication via x-api-key or Authorization Bearer token. ## GET /api/admins/privy Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/admins/privy": { "get": { "description": "Returns Privy login statistics for a given time period. Results include counts for new accounts (created_at), active accounts (latest_verified_at), total Privy accounts across all time, and the full, unmodified Privy account objects. See [Privy User object documentation](https://docs.privy.io/api-reference/users/get-all) for the complete type definition. Defaults to period=all (no date filter). Requires the authenticated account to be a Recoup admin. Authentication via x-api-key or Authorization Bearer token.", "parameters": [ { "name": "period", "in": "query", "description": "Time period to filter logins. One of: all (no date filter), daily (last 24 hours), weekly (last 7 days), monthly (last 30 days). Defaults to all.", "required": false, "schema": { "type": "string", "enum": [ "all", "daily", "weekly", "monthly" ], "default": "all" } } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Privy login statistics retrieved successfully", "content": { "application/json": { "schema": { "type": "object", "required": [ "status", "total", "total_new", "total_active", "logins" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "total": { "type": "integer", "description": "Total number of accounts in Privy across all time (not filtered by period)", "example": 42 }, "total_new": { "type": "integer", "description": "Number of accounts created (created_at) within the requested period", "example": 15 }, "total_active": { "type": "integer", "description": "Total number of accounts matching either new or active criteria in the requested period", "example": 30 }, "logins": { "type": "array", "description": "Full, unmodified Privy account objects. See [Privy User object docs](https://docs.privy.io/api-reference/users/get-all) for the complete type definition.", "items": { "type": "object" } } } } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "403": { "description": "Forbidden - authenticated account is not a Recoup admin", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } } } } } }, "components": { "schemas": { "AccountErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # List Coding Agent Slack Tags (Admin) Source: https://recoupable.dev/docs/api-reference/admins/coding-agent-slack-tags Returns a list of Slack mentions of the Recoup Coding Agent bot, pulled directly from the Slack API as the source of truth. Each entry includes the tagger's information, the prompt they sent, the timestamp, the channel, and any GitHub pull request URLs opened in response. Also returns aggregate pull request statistics. Supports optional time-period filtering. Requires the authenticated account to be a Recoup admin. Authentication via x-api-key or Authorization Bearer token. ## GET /api/admins/coding/slack Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/admins/coding/slack": { "get": { "description": "Returns a list of Slack mentions of the Recoup Coding Agent bot, pulled directly from the Slack API as the source of truth. Each entry includes the tagger's information, the prompt they sent, the timestamp, the channel, and any GitHub pull request URLs opened in response. Also returns aggregate pull request statistics. Supports optional time-period filtering. Requires the authenticated account to be a Recoup admin. Authentication via x-api-key or Authorization Bearer token.", "parameters": [ { "name": "period", "in": "query", "description": "Time period to filter tags. One of: all (no date filter), daily (last 24 hours), weekly (last 7 days), monthly (last 30 days). Defaults to all.", "required": false, "schema": { "type": "string", "enum": [ "all", "daily", "weekly", "monthly" ], "default": "all" } } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Slack tag analytics retrieved successfully", "content": { "application/json": { "schema": { "type": "object", "required": [ "status", "total", "total_pull_requests", "tags_with_pull_requests", "tags" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "total": { "type": "integer", "description": "Total number of times the Coding Agent was tagged in the requested period", "example": 134 }, "total_pull_requests": { "type": "integer", "description": "Total number of pull requests opened by the Coding Agent across all tags in the requested period", "example": 121 }, "tags_with_pull_requests": { "type": "integer", "description": "Number of tags that resulted in at least one pull request being opened", "example": 84 }, "tags": { "type": "array", "description": "List of Slack tag events", "items": { "type": "object", "required": [ "user_id", "user_name", "prompt", "timestamp", "channel_id", "channel_name" ], "properties": { "user_id": { "type": "string", "description": "Slack ID of the person who tagged the agent", "example": "U012AB3CD" }, "user_name": { "type": "string", "description": "Display name of the person who tagged the agent", "example": "Jane Smith" }, "user_avatar": { "type": [ "string", "null" ], "description": "URL of the Slack avatar", "example": "https://avatars.slack-edge.com/..." }, "prompt": { "type": "string", "description": "The text of the message sent to the agent", "example": "add dark mode support to the settings page" }, "timestamp": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp of the tag event", "example": "2024-01-15T10:30:00.000Z" }, "channel_id": { "type": "string", "description": "Slack channel ID where the tag occurred", "example": "C012AB3CD" }, "channel_name": { "type": "string", "description": "Human-readable name of the Slack channel", "example": "dev-team" }, "pull_requests": { "type": "array", "description": "GitHub pull request URLs opened by the Coding Agent in response to this prompt, parsed from bot replies in the Slack thread", "items": { "type": "string", "format": "uri", "example": "https://github.com/recoupable/api/pull/42" }, "example": [ "https://github.com/recoupable/api/pull/42" ] } } } } } } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "403": { "description": "Forbidden - authenticated account is not a Recoup admin", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } } } } } }, "components": { "schemas": { "AccountErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Coding Agent PR Merged Status (Admin) Source: https://recoupable.dev/docs/api-reference/admins/coding-pr Returns the status (open, closed, or merged) for each provided GitHub pull request URL. Accepts one or more `pull_requests` query parameters containing GitHub PR URLs. Uses the GitHub REST API to check each pull request's state. Requires the authenticated account to be a Recoup admin. Authentication via x-api-key or Authorization Bearer token. ## GET /api/admins/coding/pr Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/admins/coding/pr": { "get": { "description": "Returns the status (open, closed, or merged) for each provided GitHub pull request URL. Accepts one or more `pull_requests` query parameters containing GitHub PR URLs. Uses the GitHub REST API to check each pull request's state. Requires the authenticated account to be a Recoup admin. Authentication via x-api-key or Authorization Bearer token.", "parameters": [ { "name": "pull_requests", "in": "query", "description": "One or more GitHub pull request URLs to check. Repeat this parameter for each URL. Example: ?pull_requests=https://github.com/org/repo/pull/1&pull_requests=https://github.com/org/repo/pull/2", "required": true, "schema": { "type": "array", "items": { "type": "string", "format": "uri" }, "minItems": 1, "maxItems": 50 }, "style": "form", "explode": true } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "PR merged status retrieved successfully", "content": { "application/json": { "schema": { "type": "object", "required": [ "status", "pull_requests" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "pull_requests": { "type": "array", "description": "Status for each provided pull request URL", "items": { "type": "object", "required": [ "url", "status" ], "properties": { "url": { "type": "string", "format": "uri", "description": "The GitHub pull request URL", "example": "https://github.com/recoupable/api/pull/42" }, "status": { "type": "string", "enum": [ "open", "closed", "merged" ], "description": "The current status of the pull request", "example": "merged" } } } } } } } } }, "400": { "description": "Bad request - missing or invalid pull_requests parameter", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "403": { "description": "Forbidden - authenticated account is not a Recoup admin", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } } } } } }, "components": { "schemas": { "AccountErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # List Content Agent Slack Tags (Admin) Source: https://recoupable.dev/docs/api-reference/admins/content-slack-tags Returns a list of Slack mentions of the Recoup Content Agent bot, pulled directly from the Slack API as the source of truth. Each entry includes the tagger's information, the prompt they sent, the timestamp, the channel, and any video link responses. Also returns aggregate video statistics for measuring tag-to-video conversion. Supports optional time-period filtering. Requires the authenticated account to be a Recoup admin. Authentication via x-api-key or Authorization Bearer token. ## GET /api/admins/content/slack Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/admins/content/slack": { "get": { "description": "Returns a list of Slack mentions of the Recoup Content Agent bot, pulled directly from the Slack API as the source of truth. Each entry includes the tagger's information, the prompt they sent, the timestamp, the channel, and any video link responses. Also returns aggregate video statistics for measuring tag-to-video conversion. Supports optional time-period filtering. Requires the authenticated account to be a Recoup admin. Authentication via x-api-key or Authorization Bearer token.", "parameters": [ { "name": "period", "in": "query", "description": "Time period to filter tags. One of: all (no date filter), daily (last 24 hours), weekly (last 7 days), monthly (last 30 days). Defaults to all.", "required": false, "schema": { "type": "string", "enum": [ "all", "daily", "weekly", "monthly" ], "default": "all" } } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Slack tag analytics retrieved successfully", "content": { "application/json": { "schema": { "type": "object", "required": [ "status", "total", "total_videos", "tags_with_videos", "tags" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "total": { "type": "integer", "description": "Total number of times the Content Agent was tagged in the requested period", "example": 18 }, "total_videos": { "type": "integer", "description": "Total number of videos generated by the Content Agent across all tags in the requested period", "example": 12 }, "tags_with_videos": { "type": "integer", "description": "Number of tags that resulted in at least one video being generated", "example": 10 }, "tags": { "type": "array", "description": "List of Slack tag events", "items": { "type": "object", "required": [ "user_id", "user_name", "prompt", "timestamp", "channel_id", "channel_name" ], "properties": { "user_id": { "type": "string", "description": "Slack ID of the person who tagged the agent", "example": "U012AB3CD" }, "user_name": { "type": "string", "description": "Display name of the person who tagged the agent", "example": "Jane Smith" }, "user_avatar": { "type": [ "string", "null" ], "description": "URL of the Slack avatar", "example": "https://avatars.slack-edge.com/..." }, "prompt": { "type": "string", "description": "The text of the message sent to the agent", "example": "create a highlight reel for the new single release" }, "timestamp": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp of the tag event", "example": "2024-01-15T10:30:00.000Z" }, "channel_id": { "type": "string", "description": "Slack channel ID where the tag occurred", "example": "C012AB3CD" }, "channel_name": { "type": "string", "description": "Human-readable name of the Slack channel", "example": "content-team" }, "video_links": { "type": "array", "description": "Video URLs generated by the Content Agent in response to this prompt, parsed from bot replies in the Slack thread", "items": { "type": "string", "format": "uri", "example": "https://recoupable.com/v/abc123" }, "example": [ "https://recoupable.com/v/abc123" ] } } } } } } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "403": { "description": "Forbidden - authenticated account is not a Recoup admin", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } } } } } }, "components": { "schemas": { "AccountErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # List Agent Sign-Ups (Admin) Source: https://recoupable.dev/docs/api-reference/admins/agent-signups **Availability: pending reconciliation.** The admin signup reporting route has no matching route in the inspected API source. Its supported replacement or restoration remains to be resolved. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation. ## GET /api/admins/agent/signups Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/admins/agent/signups": { "get": { "description": "**Availability: pending reconciliation.** The admin signup reporting route has no matching route in the inspected API source. Its supported replacement or restoration remains to be resolved. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation.\n\nReturns API key sign-up records created by AI agents. Agent sign-ups are identified by the agent+ email prefix in the associated account email. Supports period filtering and returns individual API key records with their associated email and creation timestamp, suitable for building sign-up trend charts. Requires admin authentication.", "parameters": [ { "name": "period", "in": "query", "description": "Time period to filter sign-ups. One of: all (no date filter), daily (last 24 hours), weekly (last 7 days), monthly (last 30 days). Defaults to all.", "required": false, "schema": { "type": "string", "enum": [ "all", "daily", "weekly", "monthly" ], "default": "all" } } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Agent sign-up records retrieved successfully", "content": { "application/json": { "schema": { "type": "object", "required": [ "status", "total", "signups" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "total": { "type": "integer", "description": "Total number of agent API key sign-ups matching the period filter", "example": 42 }, "signups": { "type": "array", "description": "Array of agent API key sign-up records, ordered by creation date descending", "items": { "type": "object", "required": [ "id", "name", "email", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier of the API key record" }, "name": { "type": "string", "description": "Name given to the API key" }, "email": { "type": "string", "format": "email", "description": "The agent+ prefixed email used to create the API key" }, "created_at": { "type": "string", "format": "date-time", "description": "Timestamp when the API key was created" } } } } } } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "403": { "description": "Forbidden - authenticated account is not an admin", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } } }, "x-recoup-availability": "pending" } } }, "components": { "schemas": { "AccountErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # List Pro Artists (Admin) Source: https://recoupable.dev/docs/api-reference/admins/artists-pro Retrieve the deduplicated list of artist IDs owned by pro accounts. An account is "pro" when its email belongs to an enterprise domain or it has an active Stripe subscription. Admin-scoped: requires an admin-flagged API key because the response identifies paying customer accounts. ## GET /api/admins/artists/pro Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/admins/artists/pro": { "get": { "description": "Retrieve the deduplicated list of artist IDs owned by pro accounts. An account is \"pro\" when its email belongs to an enterprise domain or it has an active Stripe subscription. Admin-scoped: requires an admin-flagged API key because the response identifies paying customer accounts.", "security": [ { "apiKeyAuth": [] } ], "responses": { "200": { "description": "Pro artist IDs retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistsProResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid authentication" }, "403": { "description": "Forbidden - caller is authenticated but not admin-scoped" }, "500": { "description": "Internal server error while fetching pro artists", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtistsProResponse" } } } } } } } }, "components": { "schemas": { "ArtistsProResponse": { "type": "object", "required": [ "status", "artists" ], "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "artists": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "List of artist IDs owned by pro accounts" }, "error": { "type": "string", "description": "Error message (only present if status is 'error')" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." } } } } ``` # Credit Usage Rollup (Admin) Source: https://recoupable.dev/docs/api-reference/admins/credits-rollup Returns the per-account rollup of credit usage over the selected period, sorted by total credits deducted descending. Each row carries the account identity (UUID + display name + primary email) and the aggregated spend so the admin dashboard can render a top-spenders table without joining client-side. Pair with [`GET /api/admins/credits/events?account_id=…&period=…`](/docs/api-reference/admins/credits-events) to drill into the individual `usage_events` rows for a single account. Requires the authenticated account to be a Recoup admin. ## GET /api/admins/credits/rollup Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/admins/credits/rollup": { "get": { "description": "Returns the per-account rollup of credit usage over the selected period, sorted by total credits deducted descending. Each row carries the account identity (UUID + display name + primary email) and the aggregated spend so the admin dashboard can render a top-spenders table without joining client-side. Pair with [`GET /api/admins/credits/events?account_id=…&period=…`](/docs/api-reference/admins/credits-events) to drill into the individual `usage_events` rows for a single account. Requires the authenticated account to be a Recoup admin.", "parameters": [ { "name": "period", "in": "query", "required": false, "description": "Rollup window. `daily` = today (last 1 day), `weekly` = last 7 days, `monthly` = last 30 days, `all` = no cutoff (every event ever recorded). Defaults to `monthly`. Follows the existing admin period convention.", "schema": { "type": "string", "enum": [ "all", "daily", "weekly", "monthly" ], "default": "monthly" } }, { "name": "limit", "in": "query", "required": false, "description": "Page size — number of accounts to return per request, sorted by total credits deducted descending. Defaults to 100; max 500.", "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 100 } }, { "name": "page", "in": "query", "required": false, "description": "1-indexed page number. Server returns rows `(page - 1) * limit` through `page * limit - 1`. Defaults to 1. Use `total_count` in the response to drive a 'load more' control (`page * limit < total_count`).", "schema": { "type": "integer", "minimum": 1, "default": 1 } } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Credit usage rollup retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AdminCreditsRollupResponse" } } } }, "400": { "description": "Invalid `period` or `limit` parameter", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials" }, "403": { "description": "Forbidden - authenticated account is not a Recoup admin" }, "500": { "description": "Internal server error while computing the rollup", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } } } } } }, "components": { "schemas": { "AdminCreditsRollupResponse": { "type": "object", "required": [ "status", "period", "page", "limit", "total_count", "rows" ], "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "period": { "type": "string", "enum": [ "all", "daily", "weekly", "monthly" ], "description": "The period the response was computed over (echoes the request)" }, "page": { "type": "integer", "minimum": 1, "description": "1-indexed page returned (echoes the request, or 1 if omitted)" }, "limit": { "type": "integer", "minimum": 1, "description": "Page size used for this response (echoes the request, or the default if omitted)" }, "total_count": { "type": "integer", "minimum": 0, "description": "Total number of distinct accounts with at least one `usage_events` row in the selected period (i.e. the size of the full result set before pagination). The client derives `has_more` as `page * limit < total_count` and shows 'X of Y accounts'. Computed via a `COUNT(DISTINCT account_id)` query alongside the paginated row fetch." }, "rows": { "type": "array", "description": "Per-account credit usage totals, sorted by `total_credits_deducted` descending", "items": { "type": "object", "required": [ "account_id", "total_credits_deducted", "event_count" ], "properties": { "account_id": { "type": "string", "format": "uuid", "description": "UUID of the account" }, "account_name": { "type": "string", "nullable": true, "description": "Display name from the `accounts` table; null if the account has no name set" }, "account_email": { "type": "string", "format": "email", "nullable": true, "description": "Primary email from `account_emails` joined for human-readable rows; null if no email is on file" }, "total_credits_deducted": { "type": "integer", "description": "Sum of `usage_events.credits_deducted` across every event for this account in the selected period, in micro-dollars (1,000,000 = $1.00)" }, "event_count": { "type": "integer", "description": "Number of `usage_events` rows aggregated into this total" } } } }, "error": { "type": "string", "description": "Error message (only present if status is 'error')" } } }, "AccountErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Credit Usage Events (Admin) Source: https://recoupable.dev/docs/api-reference/admins/credits-events Returns the raw `usage_events` rows for a single account over the selected period, sorted by `created_at` descending. Powers the drilldown view that expands when a row in the [`/api/admins/credits/rollup`](/docs/api-reference/admins/credits-rollup) table is opened. Each event represents one debit (main agent turn, subagent step, chat completion, or research call), with token counts and `credits_deducted` matching the wallet drop on `credits_usage` for that account. ## GET /api/admins/credits/events Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/admins/credits/events": { "get": { "description": "Returns the raw `usage_events` rows for a single account over the selected period, sorted by `created_at` descending. Powers the drilldown view that expands when a row in the [`/api/admins/credits/rollup`](/docs/api-reference/admins/credits-rollup) table is opened. Each event represents one debit (main agent turn, subagent step, chat completion, or research call), with token counts and `credits_deducted` matching the wallet drop on `credits_usage` for that account.\n\nThe response also carries a `grants` array covering the same account and period: the admin credit grants made through [`POST /api/admins/credits`](/docs/api-reference/admins/credits-grant). The two arrays answer different questions — `events` is what the system spent, `grants` is what a person set by hand, and only a grant carries an actor and a reason. Requires the authenticated account to be a Recoup admin.", "parameters": [ { "name": "account_id", "in": "query", "required": true, "description": "UUID of the account whose `usage_events` rows to return.", "schema": { "type": "string", "format": "uuid" } }, { "name": "period", "in": "query", "required": false, "description": "Window to filter `created_at` against. Same semantics as the rollup endpoint. Defaults to `monthly`.", "schema": { "type": "string", "enum": [ "all", "daily", "weekly", "monthly" ], "default": "monthly" } }, { "name": "limit", "in": "query", "required": false, "description": "Page size — number of events to return per request, sorted by `created_at` descending. Defaults to 100; max 500.", "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 100 } }, { "name": "page", "in": "query", "required": false, "description": "1-indexed page number. Server returns rows `(page - 1) * limit` through `page * limit - 1`. Defaults to 1. Use `total_count` in the response to drive a 'load more' control on the drilldown (`page * limit < total_count`).", "schema": { "type": "integer", "minimum": 1, "default": 1 } } ], "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "responses": { "200": { "description": "Usage events retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AdminCreditsEventsResponse" } } } }, "400": { "description": "Missing or invalid `account_id`, `period`, or `limit`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials" }, "403": { "description": "Forbidden - authenticated account is not a Recoup admin" }, "500": { "description": "Internal server error while fetching usage events", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } } } } } }, "components": { "schemas": { "AdminCreditsEventsResponse": { "type": "object", "required": [ "status", "account_id", "period", "page", "limit", "total_count", "events", "grants" ], "properties": { "status": { "type": "string", "enum": [ "success", "error" ], "description": "Status of the request" }, "account_id": { "type": "string", "format": "uuid", "description": "UUID of the account whose events were returned (echoes the request)" }, "period": { "type": "string", "enum": [ "all", "daily", "weekly", "monthly" ], "description": "The period the response was filtered to (echoes the request)" }, "page": { "type": "integer", "minimum": 1, "description": "1-indexed page returned (echoes the request, or 1 if omitted)" }, "limit": { "type": "integer", "minimum": 1, "description": "Page size used for this response (echoes the request, or the default if omitted)" }, "total_count": { "type": "integer", "minimum": 0, "description": "Total number of `usage_events` rows for this account in the selected period (i.e. the size of the full result set before pagination). The client derives `has_more` as `page * limit < total_count` and shows 'X of Y events'. Computed via a `COUNT(*)` query alongside the paginated row fetch." }, "events": { "type": "array", "description": "Raw `usage_events` rows, sorted by `created_at` descending", "items": { "type": "object", "required": [ "id", "created_at", "source", "agent_type", "input_tokens", "cached_input_tokens", "output_tokens", "tool_call_count", "credits_deducted" ], "properties": { "id": { "type": "string", "description": "nanoid primary key of the `usage_events` row" }, "created_at": { "type": "string", "format": "date-time", "description": "When the event was recorded" }, "source": { "type": "string", "enum": [ "web", "api" ], "description": "Which surface originated the debit. `web` = open-agents chat. `api` = recoupable api (chat completion or research call)." }, "agent_type": { "type": "string", "enum": [ "main", "subagent" ], "description": "`main` for the top-level agent turn or any non-agent debit; `subagent` for a nested task-tool step" }, "provider": { "type": "string", "nullable": true, "description": "Provider slug (e.g. `anthropic`). Null when the debit isn't tied to a specific provider (e.g. research)." }, "model_id": { "type": "string", "nullable": true, "description": "The model for LLM-backed debits (e.g. `anthropic/claude-opus-4.6`), otherwise the billed API endpoint as `METHOD /route/pattern` (e.g. `POST /api/research/web`). Null on rows written before 2026-08-27." }, "input_tokens": { "type": "integer", "description": "Total input tokens for the call (including cached). 0 for non-LLM debits." }, "cached_input_tokens": { "type": "integer", "description": "Subset of `input_tokens` that hit the provider cache (priced at the cache-read rate)." }, "output_tokens": { "type": "integer", "description": "Output tokens for the call. 0 for non-LLM debits." }, "tool_call_count": { "type": "integer", "description": "Number of tool calls observed in this step" }, "credits_deducted": { "type": "integer", "description": "The amount debited from `credits_usage.remaining_credits` for this event, in micro-dollars (1,000,000 = $1.00). Same value the wallet dropped by on this turn." }, "resource_url": { "type": "string", "nullable": true, "description": "App-relative path of what produced the debit (`/chat?roomId=`, `/music/`, `/tasks//runs/`). Null for plain API calls and for rows written before the field existed." } } } }, "grants": { "type": "array", "description": "Admin credit grants made against this account in the selected period, sorted by `created_at` descending — the write-side counterpart to `events`. A grant is a staff member setting the balance through [`POST /api/admins/credits`](/docs/api-reference/admins/credits-grant), so unlike an event it carries an actor and a reason. Not paginated by `page`/`limit` and not counted in `total_count`, which describe `usage_events` only; capped at the 500 most recent grants in the period. Empty for the overwhelming majority of accounts, which have never been granted anything.", "items": { "type": "object", "required": [ "id", "account_id", "created_at", "granted_by", "reason", "previous_credits", "remaining_credits" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "UUID of the grant row — the `grant_id` returned by `POST /api/admins/credits`" }, "account_id": { "type": "string", "format": "uuid", "description": "UUID of the account the grant was made against. Always equal to the top-level `account_id`, since the endpoint reports on one account at a time; present because each entry is the full stored grant row." }, "created_at": { "type": "string", "format": "date-time", "description": "When the grant was made" }, "granted_by": { "type": "string", "format": "uuid", "description": "UUID of the admin account that made the grant" }, "reason": { "type": "string", "description": "The reason the admin recorded for the grant, verbatim" }, "previous_credits": { "type": "integer", "nullable": true, "description": "Balance immediately before the grant. Null when the account had no credits row and the grant created one." }, "remaining_credits": { "type": "integer", "description": "Balance the account was left holding by the grant" } } } }, "error": { "type": "string", "description": "Error message (only present if status is 'error')" } } }, "AccountErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Grant Credits (Admin) Source: https://recoupable.dev/docs/api-reference/admins/credits-grant Sets an account's credit balance, and records who set it and why. Requires the authenticated account to be a Recoup admin. ## POST /api/admins/credits Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/admins/credits": { "post": { "description": "Sets an account's credit balance, and records who set it and why. Requires the authenticated account to be a Recoup admin.\n\n**Absolute, not a delta.** `remaining_credits` is the balance the account is left holding, whatever it held before — the outcome never depends on the balance at the moment the request lands. The prior balance comes back as `previous_credits` for the record. There is deliberately no add/subtract variant.\n\n**Every grant is audited.** `reason` is required and must be non-empty. The grant is stored against the acting admin's account ID and is readable afterwards in the `grants` array of [`GET /api/admins/credits/events`](/docs/api-reference/admins/credits-events).\n\n**A grant is never reduced by the monthly refill.** Credit balances are topped up to the plan total by a monthly refill that runs lazily on the next read of [`GET /api/accounts/{id}/credits`](/docs/api-reference/accounts/credits-get), once the underlying row is more than a month old. That refill is a floor, not an assignment: it raises a balance up to the plan total and never lowers one. A grant above the plan total therefore survives every refill and holds until it is spent, so headroom granted once stays granted and does not need re-granting on a schedule.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "The account to set, the balance to leave it at, and why", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AdminGrantCreditsRequest" } } } }, "responses": { "200": { "description": "Balance set and the grant recorded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AdminGrantCreditsResponse" } } } }, "400": { "description": "Malformed JSON body, or a missing/invalid `account_id`, `remaining_credits`, or `reason`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AdminGrantCreditsValidationError" } } } }, "401": { "description": "Unauthorized - missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, "403": { "description": "Forbidden - authenticated account is not a Recoup admin", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "404": { "description": "No account exists with the supplied `account_id`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } }, "500": { "description": "Internal server error while setting the balance or recording the grant", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountErrorResponse" } } } } } } } }, "components": { "schemas": { "AdminGrantCreditsRequest": { "type": "object", "required": [ "account_id", "remaining_credits", "reason" ], "properties": { "account_id": { "type": "string", "format": "uuid", "description": "UUID of the account whose balance is being set. Must be an existing account — an unknown UUID is a 404, not a silent no-op." }, "remaining_credits": { "type": "integer", "minimum": 0, "description": "The balance to leave the account holding, in credits (1 credit = 1 US cent). Absolute, not a delta. Zero is allowed — it is how an account is deliberately zeroed out. Negative values are rejected with a 400, even though ordinary usage can overdraw a balance below zero on its own." }, "reason": { "type": "string", "minLength": 1, "description": "Why the grant was made, in plain language — e.g. `Trial headroom for the Aug 12 label demo`. Required and non-empty (a whitespace-only string is rejected): this is the field that makes a grant distinguishable from a Stripe top-up or a monthly reset when someone asks months later. Stored verbatim and returned by the events endpoint." } } }, "AdminGrantCreditsResponse": { "type": "object", "required": [ "status", "grant_id", "account_id", "remaining_credits", "previous_credits", "reason", "granted_by", "granted_at" ], "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "grant_id": { "type": "string", "format": "uuid", "description": "UUID of the recorded grant row. Matches the `id` of the corresponding entry in the `grants` array of `GET /api/admins/credits/events`." }, "account_id": { "type": "string", "format": "uuid", "description": "UUID of the account whose balance was set (echoes the request)" }, "remaining_credits": { "type": "integer", "description": "The balance the account now holds — the value supplied in the request" }, "previous_credits": { "type": "integer", "nullable": true, "description": "The balance immediately before the grant. Null when the account had no credits row at all and one was created by this request." }, "reason": { "type": "string", "description": "The reason recorded with the grant (echoes the request)" }, "granted_by": { "type": "string", "format": "uuid", "description": "UUID of the admin account that made the grant, resolved from the credentials on the request. Never taken from the body." }, "granted_at": { "type": "string", "format": "date-time", "description": "When the grant was recorded" } } }, "AdminGrantCreditsValidationError": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "missing_fields": { "type": "array", "description": "JSON path segments of the first field that failed validation, e.g. `[\"reason\"]`. Absent when the body was not valid JSON at all.", "items": { "oneOf": [ { "type": "string" }, { "type": "integer" } ] } }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "ErrorEnvelope": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "error": { "type": "string", "description": "Error message describing what went wrong" } } }, "AccountErrorResponse": { "type": "object", "required": [ "status", "message" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Status of the request" }, "message": { "type": "string", "description": "Error message describing what went wrong" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Send Email Source: https://recoupable.dev/docs/api-reference/emails/create Send an email to one or more recipients. Emails are sent from `Agent by Recoup `. ## POST /api/emails Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json ## Authentication The supplied specification does not declare an OpenAPI security object for this operation. Its declared headers and parameters still apply. Consult the authentication guide and the full specification before calling it. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Accounts", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/emails": { "post": { "description": "Send an email to one or more recipients. Emails are sent from `Agent by Recoup `.", "requestBody": { "description": "Email payload", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendEmailRequest" } } } }, "responses": { "200": { "description": "Email sent successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EmailResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Authentication required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "502": { "description": "Email delivery failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "SendEmailRequest": { "type": "object", "properties": { "to": { "type": "array", "minItems": 1, "items": { "type": "string", "format": "email" }, "description": "Recipient email address(es). Optional — when omitted, the email is sent to the authenticated account's own email address. Without a payment method on file, restricted to the account's own email; [add a payment method](/docs/api-reference/credits/sessions-create) to send to any recipient.", "example": [ "recipient@example.com" ] }, "cc": { "type": "array", "items": { "type": "string", "format": "email" }, "description": "Optional CC email addresses. Without a payment method on file, restricted to the account's own email; [add a payment method](/docs/api-reference/credits/sessions-create) to send to any recipient.", "example": [ "cc@example.com" ] }, "subject": { "type": "string", "description": "Email subject line. Optional — when omitted, defaults to the first heading or line of the body, falling back to `Message from Recoup` if the body is empty.", "example": "Weekly Pulse Report" }, "text": { "type": "string", "description": "Plain text or Markdown body. Rendered as HTML via Markdown if no `html` is provided.", "example": "# Pulse Report\n\nHere's your weekly summary." }, "html": { "type": "string", "description": "Raw HTML body. Takes precedence over `text` when both are provided.", "example": "

Pulse Report

Here's your weekly summary.

" }, "headers": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Optional custom email headers" }, "chat_id": { "type": "string", "description": "Chat ID to include a chat link in the email footer. Use [Create Chat](/docs/api-reference/chat/create) to create one." }, "account_id": { "type": "string", "format": "uuid", "description": "UUID of the account to send the email for. Only applicable when the authenticated account has access to multiple accounts via organization membership. If not provided, sends for the API key's own account." } } }, "EmailResponse": { "type": "object", "required": [ "success", "message", "id" ], "properties": { "success": { "type": "boolean", "description": "Whether the email was sent successfully", "example": true }, "message": { "type": "string", "description": "Human-readable result message", "example": "Email sent successfully from Agent by Recoup to recipient@example.com." }, "id": { "type": "string", "description": "Resend email ID", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } } } } } ``` # Get Project Source: https://recoupable.dev/docs/api-reference/projects/get Get project ## GET /api/projects/{projectId} Full OpenAPI specification: https://recoupable.dev/docs/spec/projects.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.0.1", "info": { "title": "Recoup API - Projects", "description": "Client-facing project status: the tasks on a client engagement, who they are waiting on, and the comment thread against each one.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/projects/{projectId}": { "get": { "summary": "Get project", "description": "A client project and every task on it, oldest first, with the project's collaborators. This is what the project page renders.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "projectId", "in": "path", "required": true, "description": "The project's UUID.", "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The project, its tasks and its collaborators.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProjectResponse" } } } }, "400": { "description": "Bad request — invalid path parameter or request body.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized — missing or invalid credentials. Missing or conflicting authentication headers return `status: \"error\"` with `error`. Invalid API keys, malformed Bearer headers and token-verification failures return `status: \"error\"` with `message`. Read the field provided by the response.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthenticationError" } } } }, "404": { "description": "Not found — either no project or task with this id exists, or the authenticated account is not a collaborator on it. The two cases are deliberately indistinguishable so the response cannot be used to discover which project ids are real.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal server error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "ProjectResponse": { "type": "object", "required": [ "status", "project", "tasks", "collaborators" ], "properties": { "status": { "type": "string", "example": "success" }, "project": { "$ref": "#/components/schemas/Project" }, "tasks": { "type": "array", "items": { "$ref": "#/components/schemas/ProjectTask" }, "description": "Every task on the project, oldest first. Both the active and completed sets; a client splits them on `completed_at`." }, "collaborators": { "type": "array", "items": { "$ref": "#/components/schemas/ProjectCollaborator" } } } }, "Error": { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "error": { "description": "Human-readable error message or documented error code.", "type": "string" } }, "required": [ "status", "error" ], "additionalProperties": {} }, "AuthenticationError": { "anyOf": [ { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "error": { "description": "Human-readable error message or documented error code.", "type": "string" } }, "required": [ "status", "error" ], "additionalProperties": {} }, { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "message": { "description": "Human-readable authentication error message.", "type": "string" } }, "required": [ "status", "message" ], "additionalProperties": {} } ] }, "Project": { "type": "object", "required": [ "id", "name", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "created_at": { "type": "string", "format": "date-time" } } }, "ProjectTask": { "type": "object", "required": [ "id", "project_id", "title", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "project_id": { "type": "string", "format": "uuid" }, "title": { "type": "string" }, "description": { "type": "string", "nullable": true }, "due_date": { "type": "string", "format": "date", "nullable": true, "description": "A calendar date with no time of day, e.g. `2026-09-12`." }, "assignee_account_id": { "type": "string", "format": "uuid", "nullable": true, "description": "Who the task is waiting on. A client renders its \"needs you\" treatment when this matches the viewing account." }, "completed_at": { "type": "string", "format": "date-time", "nullable": true, "description": "Null means the task is not complete. There is no separate boolean." }, "completed_by": { "type": "string", "format": "uuid", "nullable": true }, "comment_count": { "type": "integer", "format": "int32", "description": "Number of comments on this task. Present on the project read so a list can render a count without a call per task." }, "created_at": { "type": "string", "format": "date-time", "description": "Also the sort key: tasks come back oldest first." }, "updated_at": { "type": "string", "format": "date-time" } } }, "ProjectCollaborator": { "type": "object", "required": [ "account_id" ], "properties": { "account_id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "nullable": true, "description": "The collaborator's `accounts.name`. Frequently null — nothing captures a name at sign-up today — so clients must render a fallback." } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer", "description": "A Recoup API key or a Privy access token. [Learn more](/docs/authentication)." } } } } ``` # Get Project Task Source: https://recoupable.dev/docs/api-reference/projects/task-get Get project task ## GET /api/projects/{projectId}/tasks/{taskId} Full OpenAPI specification: https://recoupable.dev/docs/spec/projects.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.0.1", "info": { "title": "Recoup API - Projects", "description": "Client-facing project status: the tasks on a client engagement, who they are waiting on, and the comment thread against each one.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/projects/{projectId}/tasks/{taskId}": { "get": { "summary": "Get project task", "description": "One task with its comment feed and the project's collaborators. This is what the task detail page renders.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "projectId", "in": "path", "required": true, "description": "The project's UUID.", "schema": { "type": "string", "format": "uuid" } }, { "name": "taskId", "in": "path", "required": true, "description": "The task's UUID.", "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The task, its comments and the project's collaborators.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProjectTaskResponse" } } } }, "400": { "description": "Bad request — invalid path parameter or request body.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized — missing or invalid credentials. Missing or conflicting authentication headers return `status: \"error\"` with `error`. Invalid API keys, malformed Bearer headers and token-verification failures return `status: \"error\"` with `message`. Read the field provided by the response.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthenticationError" } } } }, "404": { "description": "Not found — either no project or task with this id exists, or the authenticated account is not a collaborator on it. The two cases are deliberately indistinguishable so the response cannot be used to discover which project ids are real.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal server error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "ProjectTaskResponse": { "type": "object", "required": [ "status", "task", "comments", "collaborators" ], "properties": { "status": { "type": "string", "example": "success" }, "task": { "$ref": "#/components/schemas/ProjectTask" }, "comments": { "type": "array", "items": { "$ref": "#/components/schemas/ProjectComment" }, "description": "Oldest first, so the feed reads top to bottom." }, "collaborators": { "type": "array", "items": { "$ref": "#/components/schemas/ProjectCollaborator" } } } }, "Error": { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "error": { "description": "Human-readable error message or documented error code.", "type": "string" } }, "required": [ "status", "error" ], "additionalProperties": {} }, "AuthenticationError": { "anyOf": [ { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "error": { "description": "Human-readable error message or documented error code.", "type": "string" } }, "required": [ "status", "error" ], "additionalProperties": {} }, { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "message": { "description": "Human-readable authentication error message.", "type": "string" } }, "required": [ "status", "message" ], "additionalProperties": {} } ] }, "ProjectTask": { "type": "object", "required": [ "id", "project_id", "title", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "project_id": { "type": "string", "format": "uuid" }, "title": { "type": "string" }, "description": { "type": "string", "nullable": true }, "due_date": { "type": "string", "format": "date", "nullable": true, "description": "A calendar date with no time of day, e.g. `2026-09-12`." }, "assignee_account_id": { "type": "string", "format": "uuid", "nullable": true, "description": "Who the task is waiting on. A client renders its \"needs you\" treatment when this matches the viewing account." }, "completed_at": { "type": "string", "format": "date-time", "nullable": true, "description": "Null means the task is not complete. There is no separate boolean." }, "completed_by": { "type": "string", "format": "uuid", "nullable": true }, "comment_count": { "type": "integer", "format": "int32", "description": "Number of comments on this task. Present on the project read so a list can render a count without a call per task." }, "created_at": { "type": "string", "format": "date-time", "description": "Also the sort key: tasks come back oldest first." }, "updated_at": { "type": "string", "format": "date-time" } } }, "ProjectComment": { "type": "object", "required": [ "id", "task_id", "account_id", "body", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "task_id": { "type": "string", "format": "uuid" }, "account_id": { "type": "string", "format": "uuid" }, "author_name": { "type": "string", "nullable": true, "description": "The author's `accounts.name`, resolved server-side. Frequently null; render a fallback." }, "body": { "type": "string" }, "created_at": { "type": "string", "format": "date-time" } } }, "ProjectCollaborator": { "type": "object", "required": [ "account_id" ], "properties": { "account_id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "nullable": true, "description": "The collaborator's `accounts.name`. Frequently null — nothing captures a name at sign-up today — so clients must render a fallback." } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer", "description": "A Recoup API key or a Privy access token. [Learn more](/docs/authentication)." } } } } ``` # Create Project Task Source: https://recoupable.dev/docs/api-reference/projects/task-create Create project task ## POST /api/projects/{projectId}/tasks Full OpenAPI specification: https://recoupable.dev/docs/spec/projects.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.0.1", "info": { "title": "Recoup API - Projects", "description": "Client-facing project status: the tasks on a client engagement, who they are waiting on, and the comment thread against each one.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/projects/{projectId}/tasks": { "post": { "summary": "Create project task", "description": "Add a task to a project. New tasks are always incomplete; set `completed` with [the update endpoint](/docs/api-reference/projects/task-update) instead.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "projectId", "in": "path", "required": true, "description": "The project's UUID.", "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "description": "The task to create.", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateProjectTaskRequest" }, "examples": { "needsClient": { "summary": "A task waiting on the client", "value": { "title": "Distribution portal access", "description": "Create a read only login for your distributor dashboard and share it with us.", "due_date": "2026-09-12", "assignee_account_id": "0f0e6d8a-1b2c-4d3e-8f90-a1b2c3d4e5f6" } } } } } }, "responses": { "201": { "description": "Task created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProjectTaskMutationResponse" } } } }, "400": { "description": "Bad request — invalid path parameter or request body.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized — missing or invalid credentials. Missing or conflicting authentication headers return `status: \"error\"` with `error`. Invalid API keys, malformed Bearer headers and token-verification failures return `status: \"error\"` with `message`. Read the field provided by the response.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthenticationError" } } } }, "404": { "description": "Not found — either no project or task with this id exists, or the authenticated account is not a collaborator on it. The two cases are deliberately indistinguishable so the response cannot be used to discover which project ids are real.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal server error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "CreateProjectTaskRequest": { "type": "object", "required": [ "title" ], "properties": { "title": { "type": "string", "minLength": 1 }, "description": { "type": "string", "nullable": true }, "due_date": { "type": "string", "format": "date", "nullable": true }, "assignee_account_id": { "type": "string", "format": "uuid", "nullable": true, "description": "Must be a collaborator on this project." } } }, "ProjectTaskMutationResponse": { "type": "object", "required": [ "status", "task" ], "properties": { "status": { "type": "string", "example": "success" }, "task": { "$ref": "#/components/schemas/ProjectTask" } } }, "Error": { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "error": { "description": "Human-readable error message or documented error code.", "type": "string" } }, "required": [ "status", "error" ], "additionalProperties": {} }, "AuthenticationError": { "anyOf": [ { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "error": { "description": "Human-readable error message or documented error code.", "type": "string" } }, "required": [ "status", "error" ], "additionalProperties": {} }, { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "message": { "description": "Human-readable authentication error message.", "type": "string" } }, "required": [ "status", "message" ], "additionalProperties": {} } ] }, "ProjectTask": { "type": "object", "required": [ "id", "project_id", "title", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "project_id": { "type": "string", "format": "uuid" }, "title": { "type": "string" }, "description": { "type": "string", "nullable": true }, "due_date": { "type": "string", "format": "date", "nullable": true, "description": "A calendar date with no time of day, e.g. `2026-09-12`." }, "assignee_account_id": { "type": "string", "format": "uuid", "nullable": true, "description": "Who the task is waiting on. A client renders its \"needs you\" treatment when this matches the viewing account." }, "completed_at": { "type": "string", "format": "date-time", "nullable": true, "description": "Null means the task is not complete. There is no separate boolean." }, "completed_by": { "type": "string", "format": "uuid", "nullable": true }, "comment_count": { "type": "integer", "format": "int32", "description": "Number of comments on this task. Present on the project read so a list can render a count without a call per task." }, "created_at": { "type": "string", "format": "date-time", "description": "Also the sort key: tasks come back oldest first." }, "updated_at": { "type": "string", "format": "date-time" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer", "description": "A Recoup API key or a Privy access token. [Learn more](/docs/authentication)." } } } } ``` # Update Project Task Source: https://recoupable.dev/docs/api-reference/projects/task-update Update project task ## PATCH /api/projects/{projectId}/tasks/{taskId} Full OpenAPI specification: https://recoupable.dev/docs/spec/projects.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.0.1", "info": { "title": "Recoup API - Projects", "description": "Client-facing project status: the tasks on a client engagement, who they are waiting on, and the comment thread against each one.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/projects/{projectId}/tasks/{taskId}": { "patch": { "summary": "Update project task", "description": "Update a task. Send only the fields that change.\n\nCompletion is toggled with the `completed` boolean rather than by writing a timestamp: the server stamps `completed_at` and records `completed_by` from the authenticated account, so it is never ambiguous who closed an item. Any collaborator may toggle any task, including the client.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "projectId", "in": "path", "required": true, "description": "The project's UUID.", "schema": { "type": "string", "format": "uuid" } }, { "name": "taskId", "in": "path", "required": true, "description": "The task's UUID.", "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "description": "The fields to change.", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateProjectTaskRequest" }, "examples": { "markComplete": { "summary": "Mark complete", "value": { "completed": true } }, "markIncomplete": { "summary": "Reopen", "value": { "completed": false } }, "reschedule": { "summary": "Change the due date", "value": { "due_date": "2026-09-19" } } } } } }, "responses": { "200": { "description": "Task updated.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProjectTaskMutationResponse" } } } }, "400": { "description": "Bad request — invalid path parameter or request body.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized — missing or invalid credentials. Missing or conflicting authentication headers return `status: \"error\"` with `error`. Invalid API keys, malformed Bearer headers and token-verification failures return `status: \"error\"` with `message`. Read the field provided by the response.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthenticationError" } } } }, "404": { "description": "Not found — either no project or task with this id exists, or the authenticated account is not a collaborator on it. The two cases are deliberately indistinguishable so the response cannot be used to discover which project ids are real.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal server error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "UpdateProjectTaskRequest": { "type": "object", "minProperties": 1, "description": "Every field is optional; send only what changes. An empty body is a 400.", "properties": { "title": { "type": "string", "minLength": 1 }, "description": { "type": "string", "nullable": true }, "due_date": { "type": "string", "format": "date", "nullable": true }, "assignee_account_id": { "type": "string", "format": "uuid", "nullable": true }, "completed": { "type": "boolean", "description": "`true` stamps `completed_at` with the current time and `completed_by` with the authenticated account. `false` clears both. This is the only way to change completion; `completed_at` is not writable directly." } } }, "ProjectTaskMutationResponse": { "type": "object", "required": [ "status", "task" ], "properties": { "status": { "type": "string", "example": "success" }, "task": { "$ref": "#/components/schemas/ProjectTask" } } }, "Error": { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "error": { "description": "Human-readable error message or documented error code.", "type": "string" } }, "required": [ "status", "error" ], "additionalProperties": {} }, "AuthenticationError": { "anyOf": [ { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "error": { "description": "Human-readable error message or documented error code.", "type": "string" } }, "required": [ "status", "error" ], "additionalProperties": {} }, { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "message": { "description": "Human-readable authentication error message.", "type": "string" } }, "required": [ "status", "message" ], "additionalProperties": {} } ] }, "ProjectTask": { "type": "object", "required": [ "id", "project_id", "title", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "project_id": { "type": "string", "format": "uuid" }, "title": { "type": "string" }, "description": { "type": "string", "nullable": true }, "due_date": { "type": "string", "format": "date", "nullable": true, "description": "A calendar date with no time of day, e.g. `2026-09-12`." }, "assignee_account_id": { "type": "string", "format": "uuid", "nullable": true, "description": "Who the task is waiting on. A client renders its \"needs you\" treatment when this matches the viewing account." }, "completed_at": { "type": "string", "format": "date-time", "nullable": true, "description": "Null means the task is not complete. There is no separate boolean." }, "completed_by": { "type": "string", "format": "uuid", "nullable": true }, "comment_count": { "type": "integer", "format": "int32", "description": "Number of comments on this task. Present on the project read so a list can render a count without a call per task." }, "created_at": { "type": "string", "format": "date-time", "description": "Also the sort key: tasks come back oldest first." }, "updated_at": { "type": "string", "format": "date-time" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer", "description": "A Recoup API key or a Privy access token. [Learn more](/docs/authentication)." } } } } ``` # Delete Project Task Source: https://recoupable.dev/docs/api-reference/projects/task-delete Delete project task ## DELETE /api/projects/{projectId}/tasks/{taskId} Full OpenAPI specification: https://recoupable.dev/docs/spec/projects.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.0.1", "info": { "title": "Recoup API - Projects", "description": "Client-facing project status: the tasks on a client engagement, who they are waiting on, and the comment thread against each one.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/projects/{projectId}/tasks/{taskId}": { "delete": { "summary": "Delete project task", "description": "Permanently delete a task and every comment on it.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "projectId", "in": "path", "required": true, "description": "The project's UUID.", "schema": { "type": "string", "format": "uuid" } }, { "name": "taskId", "in": "path", "required": true, "description": "The task's UUID.", "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Task deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteProjectTaskResponse" } } } }, "400": { "description": "Bad request — invalid path parameter or request body.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized — missing or invalid credentials. Missing or conflicting authentication headers return `status: \"error\"` with `error`. Invalid API keys, malformed Bearer headers and token-verification failures return `status: \"error\"` with `message`. Read the field provided by the response.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthenticationError" } } } }, "404": { "description": "Not found — either no project or task with this id exists, or the authenticated account is not a collaborator on it. The two cases are deliberately indistinguishable so the response cannot be used to discover which project ids are real.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal server error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "DeleteProjectTaskResponse": { "type": "object", "required": [ "status", "id" ], "properties": { "status": { "type": "string", "example": "success" }, "id": { "type": "string", "format": "uuid", "description": "The deleted task's id." } } }, "Error": { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "error": { "description": "Human-readable error message or documented error code.", "type": "string" } }, "required": [ "status", "error" ], "additionalProperties": {} }, "AuthenticationError": { "anyOf": [ { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "error": { "description": "Human-readable error message or documented error code.", "type": "string" } }, "required": [ "status", "error" ], "additionalProperties": {} }, { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "message": { "description": "Human-readable authentication error message.", "type": "string" } }, "required": [ "status", "message" ], "additionalProperties": {} } ] } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer", "description": "A Recoup API key or a Privy access token. [Learn more](/docs/authentication)." } } } } ``` # List Task Comments Source: https://recoupable.dev/docs/api-reference/projects/comments-get List task comments ## GET /api/projects/{projectId}/tasks/{taskId}/comments Full OpenAPI specification: https://recoupable.dev/docs/spec/projects.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.0.1", "info": { "title": "Recoup API - Projects", "description": "Client-facing project status: the tasks on a client engagement, who they are waiting on, and the comment thread against each one.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/projects/{projectId}/tasks/{taskId}/comments": { "get": { "summary": "List task comments", "description": "Every comment on a task, oldest first.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "projectId", "in": "path", "required": true, "description": "The project's UUID.", "schema": { "type": "string", "format": "uuid" } }, { "name": "taskId", "in": "path", "required": true, "description": "The task's UUID.", "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The task's comments.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProjectCommentsResponse" } } } }, "400": { "description": "Bad request — invalid path parameter or request body.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized — missing or invalid credentials. Missing or conflicting authentication headers return `status: \"error\"` with `error`. Invalid API keys, malformed Bearer headers and token-verification failures return `status: \"error\"` with `message`. Read the field provided by the response.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthenticationError" } } } }, "404": { "description": "Not found — either no project or task with this id exists, or the authenticated account is not a collaborator on it. The two cases are deliberately indistinguishable so the response cannot be used to discover which project ids are real.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal server error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "ProjectCommentsResponse": { "type": "object", "required": [ "status", "comments" ], "properties": { "status": { "type": "string", "example": "success" }, "comments": { "type": "array", "items": { "$ref": "#/components/schemas/ProjectComment" } } } }, "Error": { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "error": { "description": "Human-readable error message or documented error code.", "type": "string" } }, "required": [ "status", "error" ], "additionalProperties": {} }, "AuthenticationError": { "anyOf": [ { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "error": { "description": "Human-readable error message or documented error code.", "type": "string" } }, "required": [ "status", "error" ], "additionalProperties": {} }, { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "message": { "description": "Human-readable authentication error message.", "type": "string" } }, "required": [ "status", "message" ], "additionalProperties": {} } ] }, "ProjectComment": { "type": "object", "required": [ "id", "task_id", "account_id", "body", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "task_id": { "type": "string", "format": "uuid" }, "account_id": { "type": "string", "format": "uuid" }, "author_name": { "type": "string", "nullable": true, "description": "The author's `accounts.name`, resolved server-side. Frequently null; render a fallback." }, "body": { "type": "string" }, "created_at": { "type": "string", "format": "date-time" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer", "description": "A Recoup API key or a Privy access token. [Learn more](/docs/authentication)." } } } } ``` # Add Task Comment Source: https://recoupable.dev/docs/api-reference/projects/comments-create Add task comment ## POST /api/projects/{projectId}/tasks/{taskId}/comments Full OpenAPI specification: https://recoupable.dev/docs/spec/projects.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.0.1", "info": { "title": "Recoup API - Projects", "description": "Client-facing project status: the tasks on a client engagement, who they are waiting on, and the comment thread against each one.", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/projects/{projectId}/tasks/{taskId}/comments": { "post": { "summary": "Add task comment", "description": "Post a comment on a task, attributed to the authenticated account. Comments are append-only plain text: there is no edit and no delete.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "projectId", "in": "path", "required": true, "description": "The project's UUID.", "schema": { "type": "string", "format": "uuid" } }, { "name": "taskId", "in": "path", "required": true, "description": "The task's UUID.", "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "description": "The comment to post.", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateProjectCommentRequest" }, "examples": { "comment": { "summary": "A client reply", "value": { "body": "Got it. Sending the login over this week." } } } } } }, "responses": { "201": { "description": "Comment posted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProjectCommentResponse" } } } }, "400": { "description": "Bad request — invalid path parameter or request body.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized — missing or invalid credentials. Missing or conflicting authentication headers return `status: \"error\"` with `error`. Invalid API keys, malformed Bearer headers and token-verification failures return `status: \"error\"` with `message`. Read the field provided by the response.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthenticationError" } } } }, "404": { "description": "Not found — either no project or task with this id exists, or the authenticated account is not a collaborator on it. The two cases are deliberately indistinguishable so the response cannot be used to discover which project ids are real.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal server error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "CreateProjectCommentRequest": { "type": "object", "required": [ "body" ], "properties": { "body": { "type": "string", "minLength": 1, "maxLength": 4000, "description": "Plain text. No markdown, no mentions, no attachments. Comments are append-only: there is no edit or delete." } } }, "ProjectCommentResponse": { "type": "object", "required": [ "status", "comment" ], "properties": { "status": { "type": "string", "example": "success" }, "comment": { "$ref": "#/components/schemas/ProjectComment" } } }, "Error": { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "error": { "description": "Human-readable error message or documented error code.", "type": "string" } }, "required": [ "status", "error" ], "additionalProperties": {} }, "AuthenticationError": { "anyOf": [ { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "error": { "description": "Human-readable error message or documented error code.", "type": "string" } }, "required": [ "status", "error" ], "additionalProperties": {} }, { "type": "object", "properties": { "status": { "description": "Always `error`.", "type": "string", "enum": [ "error" ] }, "message": { "description": "Human-readable authentication error message.", "type": "string" } }, "required": [ "status", "message" ], "additionalProperties": {} } ] }, "ProjectComment": { "type": "object", "required": [ "id", "task_id", "account_id", "body", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "task_id": { "type": "string", "format": "uuid" }, "account_id": { "type": "string", "format": "uuid" }, "author_name": { "type": "string", "nullable": true, "description": "The author's `accounts.name`, resolved server-side. Frequently null; render a fallback." }, "body": { "type": "string" }, "created_at": { "type": "string", "format": "date-time" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer", "description": "A Recoup API key or a Privy access token. [Learn more](/docs/authentication)." } } } } ``` # Connection Ownership Source: https://recoupable.dev/docs/connectors/ownership The connector connection-ownership and authority contract: who owns a connection, who can act through it, and why roster membership never grants execute rights. ## Overview Connectors link third-party services (Google Sheets, TikTok, YouTube, X, LinkedIn, and others) to Recoup so agents can act on an artist's behalf. This page defines the **ownership and authority contract** for those connections: who a connection belongs to, and who is allowed to authorize, execute through, or disconnect it. This contract is the source of truth for connector access control. Endpoint request and response shapes are documented separately in the [Connectors API reference](https://recoupable.dev/docs/api-reference/connectors/list). --- ## Association ≠ Authority Artists in Recoup are **canonical and shared**. The same artist can appear in many accounts' rosters and in multiple organizations. Being associated with an artist is not the same as having authority to act through that artist's connections: - An artist appearing in an account's roster (`account_artist_ids`) grants **read and analytics access only**. - An artist appearing in an organization's roster likewise grants **read and analytics access only** to org members. - Neither form of association ever grants the right to authorize, execute, or disconnect a connection. > **Warning** > Roster membership never inherits connection authority. If Account A connects an artist's TikTok, Account B having that same artist on its roster does **not** let Account B post through that connection. --- ## Connection Scope Connections are scoped to the **relationship between an owner and an artist**, not to the bare canonical artist: | Connection type | Scoped to | | --- | --- | | Personal | The `(account, artist)` pair | | Org-shared | The `(organization, artist)` pair | The connection is keyed on the **join row** (the specific account-to-artist or org-to-artist relationship), never on the canonical artist alone. Two accounts that both roster the same artist hold two independent connection scopes for that artist. **Whoever establishes a connection owns it.** Ownership is set at authorization time and does not transfer through roster changes. Because artists stay canonical and shared, **isolation lives on the connection**: sharing an artist across accounts and orgs is safe precisely because each connection is confined to the relationship that created it. --- ## Authority Matrix | Capability | Who has it | | --- | --- | | Read / analytics on an artist | Any account or org member with the artist in their roster | | Authorize a new connection | The account (or org member, for org-shared) establishing the connection, who then owns it | | Execute actions through a connection | The owning account only; for org-shared connections, members of the owning org | | Disconnect a connection | The owning account only; for org-shared connections, members of the owning org | | Admin override | Recoup staff only (admins of the Recoup internal organization) | > **Note** > The admin override exists for support and abuse response. No customer account or organization, regardless of roster or org membership, can override another owner's connection. --- ## Relationship to the Current API Reference The [Connectors endpoint reference](https://recoupable.dev/docs/api-reference/connectors/list) currently describes connections as scoped to the authenticated account alone. That reflects behavior that shipped before this contract; the API is being re-keyed to the per-`(account, artist)` / per-`(organization, artist)` model defined here. Where the two disagree, **this page is the authority contract**. Endpoint docs will be updated as the re-keyed API ships. Endpoint request and response shapes for the re-keyed model are deliberately not documented here yet. # List Connectors Source: https://recoupable.dev/docs/api-reference/connectors/list List available connectors and their connection status. Returns all supported third-party integrations (e.g., Google Sheets, TikTok, YouTube, X (Twitter), LinkedIn) along with whether they are currently connected. ## GET /api/connectors Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/connectors": { "get": { "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "description": "List available connectors and their connection status. Returns all supported third-party integrations (e.g., Google Sheets, TikTok, YouTube, X (Twitter), LinkedIn) along with whether they are currently connected.", "parameters": [ { "name": "account_id", "in": "query", "description": "Optional account ID to get connectors for a different account (e.g., an artist or organization). The authenticated account must have access to the specified account. Omit to get your own account's connectors.", "required": false, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Connectors retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConnectorsResponse" } } } }, "400": { "description": "Bad request - invalid account_id format", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized - invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden - no access to the specified account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "ConnectorsResponse": { "type": "object", "required": [ "success", "connectors" ], "properties": { "success": { "type": "boolean" }, "connectors": { "type": "array", "items": { "$ref": "#/components/schemas/ConnectorInfo" }, "description": "List of available connectors with connection status" } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } }, "ConnectorInfo": { "type": "object", "required": [ "slug", "name", "isConnected" ], "properties": { "slug": { "type": "string", "description": "Unique identifier for the connector (e.g., 'googlesheets', 'tiktok', 'youtube', 'twitter', 'linkedin')" }, "name": { "type": "string", "description": "Human-readable name of the connector" }, "isConnected": { "type": "boolean", "description": "Whether the connector is currently connected" }, "connectedAccountId": { "type": "string", "description": "The connected account ID (only present when isConnected is true)" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Authorize Connector Source: https://recoupable.dev/docs/api-reference/connectors/authorize Generate an OAuth authorization URL for connecting a third-party service. Redirect to the returned URL to complete the OAuth flow. ## POST /api/connectors Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/connectors": { "post": { "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "description": "Generate an OAuth authorization URL for connecting a third-party service. Redirect to the returned URL to complete the OAuth flow.", "requestBody": { "description": "Authorization request details", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthorizeConnectorRequest" } } } }, "responses": { "200": { "description": "Authorization URL generated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthorizeConnectorResponse" } } } }, "400": { "description": "Bad request - invalid connector or parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized - invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden - no access to the specified account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "AuthorizeConnectorRequest": { "type": "object", "required": [ "connector" ], "properties": { "connector": { "type": "string", "description": "The connector slug to authorize (e.g., 'googlesheets', 'tiktok', 'youtube', 'twitter', 'linkedin')" }, "callback_url": { "type": "string", "format": "uri", "description": "Optional custom callback URL after OAuth completion" }, "account_id": { "type": "string", "format": "uuid", "description": "Optional account ID to connect a service for a different account (e.g., an artist or organization). Use this when connecting an artist's TikTok or other service. The authenticated account must have access. Omit to connect for your own account." } } }, "AuthorizeConnectorResponse": { "type": "object", "required": [ "success", "data" ], "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "required": [ "connector", "redirectUrl" ], "properties": { "connector": { "type": "string", "description": "The connector slug being authorized" }, "redirectUrl": { "type": "string", "format": "uri", "description": "URL to redirect to for OAuth authorization" } } } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Disconnect Connector Source: https://recoupable.dev/docs/api-reference/connectors/disconnect Disconnect a connected account from a third-party service. This revokes the OAuth connection and removes stored credentials. ## DELETE /api/connectors Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/connectors": { "delete": { "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "description": "Disconnect a connected account from a third-party service. This revokes the OAuth connection and removes stored credentials.", "requestBody": { "description": "Connection to disconnect", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DisconnectConnectorRequest" } } } }, "responses": { "200": { "description": "Connector disconnected successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DisconnectConnectorResponse" } } } }, "400": { "description": "Bad request - missing or invalid parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized - invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden - no access to the specified account or connection", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "DisconnectConnectorRequest": { "type": "object", "required": [ "connected_account_id" ], "properties": { "connected_account_id": { "type": "string", "description": "The connected account ID to disconnect (from ConnectorInfo.connectedAccountId)" }, "account_id": { "type": "string", "format": "uuid", "description": "Optional account ID when disconnecting a connection that belongs to a different account (e.g., an artist). Required when the connection was created for another account rather than your own. The authenticated account must have access." } } }, "DisconnectConnectorResponse": { "type": "object", "required": [ "success" ], "properties": { "success": { "type": "boolean" }, "message": { "type": "string", "description": "Status message" } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # List Connector Actions Source: https://recoupable.dev/docs/api-reference/connectors/list-actions List the executable actions available across the authenticated account's connectors. Each action is a single tool that can be invoked via POST /api/connectors/actions — for example, the `googlesheets` connector exposes actions like `GOOGLESHEETS_WRITE_SPREADSHEET`. Actions whose parent connector is not yet connected are returned with `isConnected: false` and cannot be executed until the connector is authorized via POST /api/connectors. ## GET /api/connectors/actions Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/connectors/actions": { "get": { "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "description": "List the executable actions available across the authenticated account's connectors. Each action is a single tool that can be invoked via POST /api/connectors/actions — for example, the `googlesheets` connector exposes actions like `GOOGLESHEETS_WRITE_SPREADSHEET`. Actions whose parent connector is not yet connected are returned with `isConnected: false` and cannot be executed until the connector is authorized via POST /api/connectors.", "parameters": [ { "name": "account_id", "in": "query", "description": "Optional account ID to list actions for a different account (e.g., an artist or organization). The authenticated account must have access. Omit to list actions for your own account.", "required": false, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Actions retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConnectorActionsResponse" } } } }, "400": { "description": "Bad request — invalid account_id format", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden — no access to the specified account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "ConnectorActionsResponse": { "type": "object", "required": [ "success", "actions" ], "properties": { "success": { "type": "boolean" }, "actions": { "type": "array", "items": { "$ref": "#/components/schemas/ConnectorAction" }, "description": "Available actions across all connectors the authenticated account has access to. Both connected and unconnected actions are returned — check `isConnected` per action before attempting execution." } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } }, "ConnectorAction": { "type": "object", "required": [ "slug", "name", "description", "parameters", "connectorSlug", "isConnected" ], "properties": { "slug": { "type": "string", "description": "Unique identifier for the action — pass this as `actionSlug` when calling POST /api/connectors/actions. Action slugs are always UPPERCASE_SNAKE_CASE (e.g. `GITHUB_CREATE_ISSUE`, `GMAIL_FETCH_EMAILS`, `GOOGLESHEETS_WRITE_SPREADSHEET`)." }, "name": { "type": "string", "description": "Human-readable action name." }, "description": { "type": "string", "description": "What the action does. Use this to pick the right action from the catalog." }, "parameters": { "type": "object", "description": "JSON Schema describing the parameters this action expects. Pass values matching this schema in the `parameters` field of the execute request.", "additionalProperties": true }, "connectorSlug": { "type": "string", "description": "Slug of the parent connector this action belongs to (e.g. 'googlesheets', 'tiktok', 'youtube', 'twitter', 'linkedin'). Matches the `slug` returned by GET /api/connectors." }, "isConnected": { "type": "boolean", "description": "Whether the parent connector is currently authorized for this account. Actions can only be executed when this is true." } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Upload Connector File Source: https://recoupable.dev/docs/api-reference/connectors/upload-file Stage an image into Connector file storage so it can be attached to a connector action that accepts a file_uploadable field. The returned descriptor is embedded in a file_uploadable array on [Execute Connector Action](/docs/api-reference/connectors/execute-action). ## POST /api/connectors/files Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/connectors/files": { "post": { "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "description": "Stage an image into Connector file storage so it can be attached to a connector action that accepts a file_uploadable field. The returned descriptor is embedded in a file_uploadable array on [Execute Connector Action](/docs/api-reference/connectors/execute-action).", "requestBody": { "description": "The image URL to stage and the action it will be attached to.", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UploadConnectorFileRequest" } } } }, "responses": { "200": { "description": "Image staged. Returns the Connector file descriptor to embed in the action's file_uploadable array.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UploadConnectorFileResponse" } } } }, "400": { "description": "Bad request — missing or invalid `url` (must be a reachable URL) or `toolSlug`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized — invalid or missing API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "502": { "description": "Upstream failure staging the file into Connector storage (the image fetch failed or Connector errored).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "UploadConnectorFileRequest": { "type": "object", "required": [ "url", "toolSlug" ], "properties": { "url": { "type": "string", "format": "uri", "description": "Publicly reachable URL of the image to stage. Fetched server-side and uploaded to Connector storage. Required." }, "toolSlug": { "type": "string", "description": "The action slug the image will be attached to, UPPERCASE_SNAKE_CASE (e.g. `LINKEDIN_CREATE_LINKED_IN_POST`). Scopes the upload to that tool/toolkit. Required." } } }, "UploadConnectorFileResponse": { "type": "object", "required": [ "success", "name", "mimetype", "s3key" ], "properties": { "success": { "type": "boolean" }, "name": { "type": "string", "description": "Stored filename." }, "mimetype": { "type": "string", "description": "Detected MIME type of the stored file." }, "s3key": { "type": "string", "description": "Connector storage key. Pass this together with `name` and `mimetype` in the action's file_uploadable array." } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Execute Connector Action Source: https://recoupable.dev/docs/api-reference/connectors/execute-action Execute a connector action with the given parameters. The `actionSlug` must come from a prior call to GET /api/connectors/actions, and `parameters` must match the `parameters` JSON Schema returned for that action. The action's parent connector must be currently connected (`isConnected: true` in the catalog) — otherwise this endpoint returns 409. The `result` field passes through whatever the underlying connector returns; its shape is action-specific. To attach an image to a `file_uploadable` parameter (e.g. `images`), first stage it with [Upload Connector File](/docs/api-reference/connectors/upload-file) and pass the returned descriptor. ## POST /api/connectors/actions Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/connectors/actions": { "post": { "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "description": "Execute a connector action with the given parameters. The `actionSlug` must come from a prior call to GET /api/connectors/actions, and `parameters` must match the `parameters` JSON Schema returned for that action. The action's parent connector must be currently connected (`isConnected: true` in the catalog) — otherwise this endpoint returns 409. The `result` field passes through whatever the underlying connector returns; its shape is action-specific. To attach an image to a `file_uploadable` parameter (e.g. `images`), first stage it with [Upload Connector File](/docs/api-reference/connectors/upload-file) and pass the returned descriptor.", "requestBody": { "description": "Action to execute and the parameters for it", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ExecuteConnectorActionRequest" } } } }, "responses": { "200": { "description": "Action executed successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ExecuteConnectorActionResponse" } } } }, "400": { "description": "Bad request — missing or invalid parameters (e.g. `parameters` does not match the action's schema)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Forbidden — no access to the specified account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Action slug not found in any of the account's connectors", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "The action's parent connector is not currently connected. Authorize it via POST /api/connectors before retrying.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "502": { "description": "Upstream connector failure (the third-party service errored or timed out)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "ExecuteConnectorActionRequest": { "type": "object", "required": [ "actionSlug", "parameters" ], "properties": { "actionSlug": { "type": "string", "description": "Action slug from GET /api/connectors/actions. Always UPPERCASE_SNAKE_CASE (e.g. `GOOGLESHEETS_WRITE_SPREADSHEET`). Required." }, "parameters": { "type": "object", "description": "Action-specific parameters matching the `parameters` JSON Schema returned by GET /api/connectors/actions for this `actionSlug`. The connector validates these against the cached schema before executing — invalid shapes return 400. Required.", "additionalProperties": true }, "account_id": { "type": "string", "format": "uuid", "description": "Optional account ID to execute the action on a different account (e.g., an artist). The authenticated account must have access. Omit to execute on your own account." } } }, "ExecuteConnectorActionResponse": { "type": "object", "required": [ "success", "result" ], "properties": { "success": { "type": "boolean" }, "result": { "type": "object", "additionalProperties": true, "description": "Pass-through of the underlying connector's response payload. Shape is action-specific — consult the action's parameters schema and the third-party service's own documentation for what to expect. The server-side wrapper does not transform this field." }, "executedAt": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp of when the action was executed server-side." } } }, "Error": { "required": [ "error", "message" ], "type": "object", "properties": { "error": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Create Post Source: https://recoupable.dev/docs/api-reference/publishing/create Create post ## POST /api/posts Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/posts": { "post": { "summary": "Create post", "description": "**Availability: pending.** This endpoint is specified ahead of implementation and is not yet live on the hosted API; it ships through recoupable/app#2241. Do not rely on it until this notice is removed.\n\nPublish a video with its copy to **one** connected social account, now or at a set time. Send one request per target account; to post the same video to several platforms, call this once per platform.\n\nWithout `publish_at` the post starts publishing immediately and is returned as `publishing`. With `publish_at` it is returned as `scheduled` and publishes at that time. Either way this returns **202** right away: poll [Get Post](/docs/api-reference/publishing/get) until `status` is `posted`, `failed` or `cancelled`.\n\nThe platform must already be connected for the account (see [Authorize Connector](/docs/api-reference/connectors/authorize)). Copy uses one shape for every platform. Which fields each platform uses, and their limits:\n\n| Platform | `title` | `description` | `cover_url` | `privacy`, `tags` |\n|---|---|---|---|---|\n| `youtube` | **required**, video title, max 100 characters | video description, max 5,000 characters | not used (YouTube picks the frame) | used |\n| `instagram` | not used | **required**, Reel caption, max 2,200 characters | Reel cover image | not used |\n| `x` | not used | **required**, post text, max 280 weighted characters (URLs count as 23) | not used | not used |\n| `linkedin` | video title shown on the post | **required**, post commentary, max 3,000 characters | video thumbnail | not used |\n\nLimits are checked here, so an over-length body fails with 400 instead of at publish time.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreatePostRequest" } } } }, "responses": { "202": { "description": "Post accepted: publishing now, or scheduled for `publish_at`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreatePostResponse" } } }, "headers": { "Location": { "schema": { "type": "string" }, "description": "URL of the created post." } } }, "400": { "description": "Validation failed: a missing or unknown field, a field the platform requires (YouTube `title`; Instagram, X and LinkedIn `description`), a body over the platform's limit, or a `publish_at` in the past. `missing_fields` names the first offending field.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } }, "401": { "description": "Unauthorized. Provide exactly one of `x-api-key` or `Authorization: Bearer`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } }, "403": { "description": "Access denied to the specified `account_id`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } }, "409": { "description": "The `platform` is not connected for this account. Authorize it via [Authorize Connector](/docs/api-reference/connectors/authorize) before retrying.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } }, "500": { "description": "Internal server error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } } }, "x-recoup-availability": "pending" } } }, "components": { "schemas": { "CreatePostRequest": { "type": "object", "required": [ "platform", "video_url" ], "description": "One post to one connected account. Copy uses the same fields for every platform; see the operation description for which fields each platform uses and their limits.", "properties": { "account_id": { "type": "string", "format": "uuid", "description": "Optional. The account whose connection posts (e.g. an artist). The authenticated account must have access. Defaults to the calling account." }, "platform": { "$ref": "#/components/schemas/PostPlatform" }, "sub_account_id": { "type": "string", "description": "Optional, LinkedIn only. Organization URN to post as a Page (e.g. `urn:li:organization:101127186`) instead of as the connected member. The connected member must be an admin of that Page." }, "video_url": { "type": "string", "format": "uri", "description": "Publicly reachable URL of the video file (MP4). It is fetched at publish time, so it must still resolve then." }, "cover_url": { "type": "string", "format": "uri", "description": "Optional. Publicly reachable image URL used as the cover or thumbnail where the platform supports one (see the table in the description)." }, "title": { "type": "string", "description": "Title. Required for `youtube`; see the table in the description." }, "description": { "type": "string", "description": "Body text: caption, post text or description, depending on the platform. Required for `instagram`, `x` and `linkedin`." }, "privacy": { "type": "string", "enum": [ "public", "unlisted", "private" ], "default": "public", "description": "Optional, YouTube only. Visibility after upload." }, "tags": { "type": "array", "items": { "type": "string" }, "description": "Optional, YouTube only. Video tags." }, "publish_at": { "type": "string", "format": "date-time", "description": "Optional. ISO 8601 time to publish, in the future. Omit to publish now." } } }, "CreatePostResponse": { "type": "object", "required": [ "id", "status" ], "description": "The post's id and current status.", "properties": { "id": { "type": "string", "format": "uuid", "description": "The post's id. Poll [Get Post](/docs/api-reference/publishing/get) with this." }, "status": { "$ref": "#/components/schemas/PostStatus" } } }, "PostErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `error`" }, "missing_fields": { "type": "array", "description": "JSON path segments to the first field that failed validation, e.g. `[\"description\"]`", "items": { "oneOf": [ { "type": "string" }, { "type": "integer" } ] } }, "error": { "type": "string", "description": "Human-readable error message" } } }, "PostPlatform": { "type": "string", "enum": [ "instagram", "x", "linkedin", "youtube" ], "description": "Target platform. The account must have this platform connected." }, "PostStatus": { "type": "string", "enum": [ "scheduled", "publishing", "posted", "failed", "cancelled" ], "description": "Lifecycle phase. `scheduled` until `publish_at`, `publishing` while the platform upload runs, then `posted` or `failed`. `cancelled` only from `scheduled`." } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # List Posts Source: https://recoupable.dev/docs/api-reference/publishing/list List posts ## GET /api/posts Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/posts": { "get": { "summary": "List posts", "description": "**Availability: pending.** This endpoint is specified ahead of implementation and is not yet live on the hosted API; it ships through recoupable/app#2241. Do not rely on it until this notice is removed.\n\nThe account's posts, newest first, with each one's status and live URL.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "account_id", "in": "query", "required": false, "schema": { "type": "string", "format": "uuid" }, "description": "Optional. Act on behalf of another account you can access. Defaults to the calling account." }, { "name": "status", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "scheduled", "publishing", "posted", "failed", "cancelled" ] }, "description": "Optional. Return only posts in this state." }, { "name": "platform", "in": "query", "required": false, "schema": { "$ref": "#/components/schemas/PostPlatform" }, "description": "Optional. Return only posts to this platform." }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 }, "description": "Optional. Maximum posts to return, newest first." }, { "name": "offset", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 0, "default": 0 }, "description": "Optional. Number of posts to skip, for paging." } ], "responses": { "200": { "description": "Posts in the requested account, newest first.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostListResponse" } } } }, "400": { "description": "Invalid query parameter.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } }, "401": { "description": "Unauthorized.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } }, "403": { "description": "Access denied to the specified `account_id`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } }, "500": { "description": "Internal server error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } } }, "x-recoup-availability": "pending" } } }, "components": { "schemas": { "PostPlatform": { "type": "string", "enum": [ "instagram", "x", "linkedin", "youtube" ], "description": "Target platform. The account must have this platform connected." }, "PostListResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "posts": { "type": "array", "description": "Posts, newest first. Empty when the account has none.", "items": { "$ref": "#/components/schemas/Post" } } } }, "PostErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `error`" }, "missing_fields": { "type": "array", "description": "JSON path segments to the first field that failed validation, e.g. `[\"description\"]`", "items": { "oneOf": [ { "type": "string" }, { "type": "integer" } ] } }, "error": { "type": "string", "description": "Human-readable error message" } } }, "Post": { "type": "object", "description": "One post to one account.", "properties": { "id": { "type": "string", "format": "uuid", "description": "The post's id." }, "account_id": { "type": "string", "format": "uuid", "description": "The account whose connection posts." }, "platform": { "$ref": "#/components/schemas/PostPlatform" }, "sub_account_id": { "type": [ "string", "null" ], "description": "LinkedIn organization URN when posting as a Page, else null." }, "status": { "$ref": "#/components/schemas/PostStatus" }, "video_url": { "type": "string", "description": "The video that is or was published." }, "cover_url": { "type": [ "string", "null" ], "description": "The cover or thumbnail image, if any." }, "title": { "type": [ "string", "null" ], "description": "Submitted title." }, "description": { "type": [ "string", "null" ], "description": "Submitted body text." }, "privacy": { "type": [ "string", "null" ], "description": "Submitted YouTube visibility." }, "tags": { "type": [ "array", "null" ], "items": { "type": "string" }, "description": "Submitted YouTube tags." }, "publish_at": { "type": [ "string", "null" ], "format": "date-time", "description": "Requested publish time. Null when published on creation." }, "post_url": { "type": [ "string", "null" ], "description": "Live URL on the platform. Null until `posted`." }, "error_message": { "type": [ "string", "null" ], "description": "Why publishing failed, as returned by the platform (e.g. a daily upload quota). Null unless `status` is `failed`." }, "text_verified": { "type": [ "boolean", "null" ], "description": "Whether the live text, re-read from the platform after posting, equals the submitted `title`/`description`. Null until checked." }, "created_at": { "type": "string", "format": "date-time", "description": "When the post was created." }, "updated_at": { "type": "string", "format": "date-time", "description": "When the post last changed." } } }, "PostStatus": { "type": "string", "enum": [ "scheduled", "publishing", "posted", "failed", "cancelled" ], "description": "Lifecycle phase. `scheduled` until `publish_at`, `publishing` while the platform upload runs, then `posted` or `failed`. `cancelled` only from `scheduled`." } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Get Post Source: https://recoupable.dev/docs/api-reference/publishing/get Get post ## GET /api/posts/{id} Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/posts/{id}": { "get": { "summary": "Get post", "description": "**Availability: pending.** This endpoint is specified ahead of implementation and is not yet live on the hosted API; it ships through recoupable/app#2241. Do not rely on it until this notice is removed.\n\nOne post, with its status, live `post_url` once posted, `error_message` if it failed, and `text_verified` once the live text has been re-read. This is the polling target after [Create Post](/docs/api-reference/publishing/create).", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The post's id, from [Create Post](/docs/api-reference/publishing/create)." }, { "name": "account_id", "in": "query", "required": false, "schema": { "type": "string", "format": "uuid" }, "description": "Optional. Act on behalf of another account you can access. Defaults to the calling account." } ], "responses": { "200": { "description": "The post.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostDetailResponse" } } } }, "400": { "description": "`id` is not a valid uuid.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } }, "401": { "description": "Unauthorized.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } }, "403": { "description": "Access denied to this post's account.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } }, "404": { "description": "No post with that id.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } }, "500": { "description": "Internal server error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } } }, "x-recoup-availability": "pending" } } }, "components": { "schemas": { "PostDetailResponse": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "success" ], "description": "Status of the request" }, "post": { "$ref": "#/components/schemas/Post" } } }, "PostErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `error`" }, "missing_fields": { "type": "array", "description": "JSON path segments to the first field that failed validation, e.g. `[\"description\"]`", "items": { "oneOf": [ { "type": "string" }, { "type": "integer" } ] } }, "error": { "type": "string", "description": "Human-readable error message" } } }, "Post": { "type": "object", "description": "One post to one account.", "properties": { "id": { "type": "string", "format": "uuid", "description": "The post's id." }, "account_id": { "type": "string", "format": "uuid", "description": "The account whose connection posts." }, "platform": { "$ref": "#/components/schemas/PostPlatform" }, "sub_account_id": { "type": [ "string", "null" ], "description": "LinkedIn organization URN when posting as a Page, else null." }, "status": { "$ref": "#/components/schemas/PostStatus" }, "video_url": { "type": "string", "description": "The video that is or was published." }, "cover_url": { "type": [ "string", "null" ], "description": "The cover or thumbnail image, if any." }, "title": { "type": [ "string", "null" ], "description": "Submitted title." }, "description": { "type": [ "string", "null" ], "description": "Submitted body text." }, "privacy": { "type": [ "string", "null" ], "description": "Submitted YouTube visibility." }, "tags": { "type": [ "array", "null" ], "items": { "type": "string" }, "description": "Submitted YouTube tags." }, "publish_at": { "type": [ "string", "null" ], "format": "date-time", "description": "Requested publish time. Null when published on creation." }, "post_url": { "type": [ "string", "null" ], "description": "Live URL on the platform. Null until `posted`." }, "error_message": { "type": [ "string", "null" ], "description": "Why publishing failed, as returned by the platform (e.g. a daily upload quota). Null unless `status` is `failed`." }, "text_verified": { "type": [ "boolean", "null" ], "description": "Whether the live text, re-read from the platform after posting, equals the submitted `title`/`description`. Null until checked." }, "created_at": { "type": "string", "format": "date-time", "description": "When the post was created." }, "updated_at": { "type": "string", "format": "date-time", "description": "When the post last changed." } } }, "PostPlatform": { "type": "string", "enum": [ "instagram", "x", "linkedin", "youtube" ], "description": "Target platform. The account must have this platform connected." }, "PostStatus": { "type": "string", "enum": [ "scheduled", "publishing", "posted", "failed", "cancelled" ], "description": "Lifecycle phase. `scheduled` until `publish_at`, `publishing` while the platform upload runs, then `posted` or `failed`. `cancelled` only from `scheduled`." } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Cancel Post Source: https://recoupable.dev/docs/api-reference/publishing/cancel Cancel post ## DELETE /api/posts/{id} Full OpenAPI specification: https://recoupable.dev/docs/spec/social.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Social", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/posts/{id}": { "delete": { "summary": "Cancel post", "description": "**Availability: pending.** This endpoint is specified ahead of implementation and is not yet live on the hosted API; it ships through recoupable/app#2241. Do not rely on it until this notice is removed.\n\nCancel a post that is still `scheduled`. Nothing is sent to the platform. A post that is already `publishing`, `posted`, `failed` or `cancelled` cannot be cancelled (409); to remove a live post, delete it on the platform.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The post's id, from [Create Post](/docs/api-reference/publishing/create)." }, { "name": "account_id", "in": "query", "required": false, "schema": { "type": "string", "format": "uuid" }, "description": "Optional. Act on behalf of another account you can access. Defaults to the calling account." } ], "responses": { "200": { "description": "The post is cancelled.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreatePostResponse" } } } }, "400": { "description": "`id` is not a valid uuid.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } }, "401": { "description": "Unauthorized.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } }, "403": { "description": "Access denied to this post's account.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } }, "404": { "description": "No post with that id.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } }, "409": { "description": "The post is not `scheduled`, so it can no longer be cancelled.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } }, "500": { "description": "Internal server error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostErrorResponse" } } } } }, "x-recoup-availability": "pending" } } }, "components": { "schemas": { "CreatePostResponse": { "type": "object", "required": [ "id", "status" ], "description": "The post's id and current status.", "properties": { "id": { "type": "string", "format": "uuid", "description": "The post's id. Poll [Get Post](/docs/api-reference/publishing/get) with this." }, "status": { "$ref": "#/components/schemas/PostStatus" } } }, "PostErrorResponse": { "type": "object", "required": [ "status", "error" ], "properties": { "status": { "type": "string", "enum": [ "error" ], "description": "Always `error`" }, "missing_fields": { "type": "array", "description": "JSON path segments to the first field that failed validation, e.g. `[\"description\"]`", "items": { "oneOf": [ { "type": "string" }, { "type": "integer" } ] } }, "error": { "type": "string", "description": "Human-readable error message" } } }, "PostStatus": { "type": "string", "enum": [ "scheduled", "publishing", "posted", "failed", "cancelled" ], "description": "Lifecycle phase. `scheduled` until `publish_at`, `publishing` while the platform upload runs, then `posted` or `failed`. `cancelled` only from `scheduled`." } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Sites Source: https://recoupable.dev/docs/api-reference/sites/overview Create, generate, and publish fan experiences through the API or MCP. Sites uses the same backend operations from the web app, HTTP API, and MCP. A new site is a **private draft**. Generation never publishes it automatically. ## Authentication Private HTTP endpoints accept exactly one of `x-api-key: YOUR_API_KEY` or `Authorization: Bearer YOUR_PRIVY_ACCESS_TOKEN`. MCP uses the existing authenticated `https://api.recoupable.dev/mcp` connection. The caller is derived from authentication. Do not send an `account_id`. An optional `organizationId` selects a workspace the caller can access. Omit it for the authenticated account's workspace. ## Endpoints and tools | HTTP | MCP tool | Result | | --- | --- | --- | | `GET /api/sites` | `list_sites` | `{ sites: [...] }` | | `POST /api/sites` | `create_site` | `{ site: ... }`, HTTP 201 | | `GET /api/sites/{id}` | `get_site` | `{ site: ... }` | | `PATCH /api/sites/{id}` action `concepts` | `propose_site_concepts` | Short pitches or missing context | | `PATCH /api/sites/{id}` action `generate` | `generate_site` | Generate from an optional customer-selected pitch; otherwise select a concept | | `PATCH /api/sites/{id}` action `publish` | `publish_site` | Saved draft becomes public | | `PATCH /api/sites/{id}` action `unpublish` | `unpublish_site` | Public snapshot removed, draft retained | | `PATCH /api/sites/{id}` action `delete` | `delete_site` | `{ "deleted": true, "id": "..." }` | | `GET /api/sites/{id}/signups` | `get_site_signups` | `{ signups: [{ email, created_at }] }`, up to 10,000 | | `POST /api/sites/assets` | `upload_site_asset` | `{ asset: { url, name, type } }` | Listing accepts optional `organizationId` and `artistId` query parameters. All private reads and writes verify workspace access. Signup emails are never part of the public response. ## Create and generate Create a draft with a Spotify track, album, or playlist URL. Recoup retrieves its title and artwork. The brief is optional; without it, Recoup uses the available song and artist context to propose activities. Alternatively, supply both a name and a brief without a release URL. ```bash curl https://api.recoupable.dev/api/sites \ -H "x-api-key: $RECOUP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"releaseUrl":"https://open.spotify.com/track/TRACK_ID","brief":"Build a maze game"}' ``` Optional creation fields: `name` (120 characters), `brief` (6,000), `organizationId`, `artistId`, and `assets` (up to eight workspace-owned assets returned by the upload endpoint). An artist must be accessible to the caller and belong to the selected organization when one is used. Read `site.id` and `site.revision`, then start automatic generation: ```json {"action":"generate","revision":0} ``` For Spotify tracks, the background workflow saves metadata, acquires and verifies full audio, saves Music Flamingo lyrics and musical analysis, collects artwork observations and public artist research, compiles a saved brief, selects a concept, and builds and reviews the site. No separate collector calls are required. Audio acquisition requires a Spotify preview for waveform verification and stops if no matching full recording is found. Completed accepted context is reused. To choose a concept yourself before generation, optionally request pitches: ```json {"action":"concepts","revision":0} ``` Send this to `PATCH /api/sites/{id}`, or call MCP `propose_site_concepts` with `id` and `revision`. Optional `instruction` and `contextBriefId` apply to pitching too. This can use credits for context collection and a pitch call, but does not create images, site code or change the draft. The response is `{ concepts: { status, candidates, reason }, revision }`. Status is `ready`, `needs-context`, or `no-good-concept`. Metadata and cover details alone return `needs-context`; provide usable song analysis or sourced artist context rather than forcing an activity out of the artwork. In the explicit-selection flow, show the short pitches to the customer and wait for their choice. Each candidate contains `name`, `activity`, `fanMotivation`, `songOrArtistConnection`, and `friendHook`. Pass the selected object unchanged as `approvedConcept`: ```json { "action": "generate", "revision": 0, "approvedConcept": { "name": "CUSTOMER_SELECTED_NAME", "activity": "SELECTED_ACTIVITY", "fanMotivation": "SELECTED_MOTIVATION", "songOrArtistConnection": "SELECTED_CONNECTION", "friendHook": "SELECTED_HOOK" } } ``` MCP `generate_site` accepts the same fields plus `id`, without `action`. A customer-authored concept is also accepted. The optional `approvedConcept` preserves a caller-selected activity. When omitted, the engine selects from its proposed concepts after collecting context. Generation defaults to background execution and returns a generation token; poll `get_site_generation` (HTTP action `generation`) for the result. Set `background: false` only for a synchronous client that supports a long timeout. If review rejects the concept, the draft is marked `needs-review`; the system does not silently substitute a new activity. Asset and implementation repairs retain their bounded pass. Generation failures identify the stopped stage and leave the saved draft unchanged. A `409` means another edit changed the revision; read the site again before retrying. ## Generate from saved Context Engine evidence Save a single-song `creative_direction` brief using the Context Engine `save_brief` action, then pass its snapshot ID when requesting pitches: ```json { "action": "concepts", "revision": 0, "contextBriefId": "SAVED_BRIEF_UUID" } ``` Pass the same `contextBriefId` with the selected `approvedConcept` to generation. It is available on both MCP `propose_site_concepts` and `generate_site`. Without a saved brief, track generation automatically collects and saves context, reusing compatible accepted module results. The brief must belong to the site's workspace and match its Spotify track. Sites rejects unavailable or superseded snapshots before generation. Revisions reuse the draft's selected brief unless a new ID is supplied. This path reuses accepted song summaries, artwork observations, public artist research and release/artist metadata instead of collecting them again. The creative director and generator receive attribution, coverage and missing-topic information. Supported evidence includes public Spotify metadata/artwork, YouTube-attributed audio analysis and saved public-search research. Private uploads, customer assertions and raw lyrics are excluded. Matching saved machine transcripts inform paraphrased themes but are not forwarded to the site. Partial evidence stays partial. Generation still uses credits for creative direction, assets, implementation and review. It saves a private draft, never publishes automatically. Snapshot availability is checked again before saving and publishing. Later source withdrawal does not automatically retract an already published site. Internal evidence and source identifiers are excluded from the public site response. ## Publish After reviewing the draft, send `{"action":"publish","revision":CURRENT_REVISION}` to the same PATCH endpoint, or call `publish_site` with `id` and `revision`. Publishing automatically prepares Spotify fan connection for an active paid workspace. The standard agreement uses the attributed artist's name; the customer does not need a separate setup step. Missing artist attribution or unavailable Spotify configuration stops paid publication with an actionable error. Publication requires an eligible paid workspace. Unpaid publication returns HTTP 402 before publishing. Successful paid setup reports `fanConnection: "enabled"`. For a custom hosted destination, HTTP callers may include `returnUrl` (an HTTPS URL without credentials or fragment) in the publish body. Otherwise an existing connection destination is preserved, or the configured Sites public origin is used. The Recoup editor supplies its actual published URL. The authenticated fan-connection API remains available for external integrations. The public page is `https://chat.recoupable.dev/s/{id}`. Unpublish with `action: "unpublish"`, or `unpublish_site`. Agents should publish only when explicitly instructed. ## Delete and cancel generation Delete only when explicitly instructed. Send `PATCH /api/sites/{id}` with `action: "delete"` and the current `revision` in its JSON request body (Content-Type: application/json). Include `generationToken` when cancelling a known in-progress generation. The token must belong to this site and caller; cancellation is attempted before deletion. Omitting it does not request cancellation of every running job. For example, use the current saved revision and the token returned by a known generation: ```json {"action":"delete","revision":3,"generationToken":"TOKEN_FROM_GENERATION"} ``` Omit generationToken when no known generation needs cancellation. Deletion checks workspace access and revision, then returns `{ "deleted": true, "id": "..." }`. A stale revision returns 409; reload before retrying. Unpublish with `action: "unpublish"` when the draft should remain available. ## Uploads HTTP uploads are multipart forms with a `file` field and optional `organizationId` query parameter. Supported formats: JPEG, PNG, WebP, MP3, and WAV, up to 4 MB per file. Images are re-encoded to WebP. Audio is checked for its file signature. MCP's `upload_site_asset` accepts `name`, `contentType`, `base64` file bytes, and optional `organizationId`. It uses the same validation and storage path as HTTP. ## Public endpoints - `GET /api/sites/public/{id}` returns `{ snapshot: ... }` for a published site only. Drafts, account identifiers, and signup data are excluded. - `POST /api/sites/public/{id}/signup` accepts `{"email":"fan@example.com","consent":"yes"}`. An optional `website` honeypot must be empty. Duplicate submissions succeed without disclosing list membership. Unpublished sites reject signups. Public pages render in the chat app. The API owns snapshots, generation, uploads, and signup persistence. Spotify OAuth and the browser music player remain in the web app. ## Errors - `402`: publication requires an eligible paid workspace. `400` invalid input; `401` missing/invalid authentication; `403` workspace or artist access denied; `404` missing/unpublished site; `409` stale revision; `422` Spotify metadata unavailable; `503` temporary operation failure. # Connect Spotify fans to a site Source: https://recoupable.dev/docs/essentials/site-fan-connection Configure paid fan connection from an artist agent and retrieve fans attributed to its site. Release dependency: these routes require the site fan connection API release and its database migration. The skill alone does not enable them. Base URL: `https://api.recoupable.dev`. Private calls use `x-api-key: $RECOUP_API_KEY` (or the platform's existing bearer authentication). Customer credentials stay with the agent/server. Public connection entry and callback use browser-bound OAuth state, not the customer API key. ## Customer and site - `GET /api/accounts/id`: verify the current credential. - `POST /api/agents/signup` with `{ "email": "customer@example.com" }`: request email verification. - `POST /api/agents/verify` with `{ "email": "customer@example.com", "code": "123456" }`: receive `api_key`; never echo it. - `GET /api/artists`: inspect the caller's roster and choose the artist's account ID. - `GET /api/sites`: inspect existing site records. - `POST /api/sites` with `{ "name": "Release experience", "artistId": "ARTIST_ACCOUNT_UUID", "releaseUrl": "https://open.spotify.com/track/TRACK_ID" }`: register a site. Add `organizationId` only for a selected authorized workspace. Read the returned `site.id`. Omit owner/account ID fields; ownership comes from authentication. ## Configure `GET /api/sites/{siteId}/fan-connection` returns: ```json { "siteId": "SITE_UUID", "ownerId": "WORKSPACE_ACCOUNT_UUID", "artistId": "ARTIST_ACCOUNT_UUID", "config": null, "connectUrl": null } ``` Existing `config` contains `site_id`, `return_url`, `marketing_text`, `enabled` and `revision`. Update with `PUT /api/sites/{siteId}/fan-connection`: ```json { "returnUrl": "https://artist.example/release", "marketingText": "I agree to receive release announcements and offers by email from Example Artist.", "enabled": true, "revision": 0 } ``` Use the real artist's approved text, not the example unchanged. Return URL must be HTTPS, at most 2048 characters, without credentials or fragments. Text length is 20–1000 characters. Pass the last read revision, or 0 for initial configuration. The response has the same envelope with persisted config and the public `connectUrl`; always use that returned URL. - 400: invalid input or missing artist attribution. - 401/403: reconnect the customer or select an authorized workspace. - 402: no eligible paid subscription for the site's owning account. - 404: missing site or API not deployed; inspect the actual response. - 409: another configuration update won; reload and reconcile before retry. - 503: provider, billing or storage unavailable; surface the dependency. Spotify fan connection is included in an active paid Recoup subscription owned by the site's workspace. No separate add-on is required. Existing `POST /api/subscriptions/sessions` accepts `{plan: "starter" | "pro", successUrl, cancelUrl?}` and returns the customer checkout URL. Let the customer complete payment; then retry activation. Fans do not buy this feature. ## Public fan journey `GET /api/sites/public/{siteId}/spotify` displays the site's agreement. Its own form POST binds acceptance to that browser and configuration revision, then redirects to Spotify PKCE authorization. Do not handcraft this POST or bypass the page. Spotify returns to Recoup's `/api/sites/spotify/callback`; Recoup then returns to the configured site URL with `recoup_spotify=connected`, `cancelled` or `failed`. Only a successful provider exchange with `user-read-email` and `user-read-private` creates records. Actual granted scopes and the exact accepted marketing text are retained separately from the successful connection. Email may be absent. No tokens or profile details are passed to the public site. ## Read fans `GET /api/sites/{siteId}/fans?offset=0&limit=50` (private): ```json { "fans": [], "total": 0, "offset": 0, "limit": 50 } ``` Each fan has `id`, `site_id`, `spotify_id`, `display_name`, `email`, first/last connection timestamps and nested `site_fan_connections` with `site_fan_permissions` and `site_fan_marketing_consents`. Paginate up to 100 fans per page. Records are private to authorized workspace members. Read access remains available after paid eligibility expires. Repeated connection updates the existing fan for that site and adds a connection history entry. The site's owner and artist supply attribution. These records are separate from email-only signups and other artist audience data. Starter Checkout can return HTTP 400 `starter_unavailable` when its price is not configured. Explain the unavailable option and use a configured plan; do not retry it as a transient provider failure. ## Recoup operator setup Operators must apply `20260928010000_site_fan_connections.sql`, configure `SITES_SPOTIFY_CLIENT_ID`, register an HTTPS callback ending `/api/sites/spotify/callback`, set `SITES_FAN_SPOTIFY_REDIRECT_URI`. This is Recoup service configuration, not work to impose on each artist. The existing browser playback callback remains separate. ## Activity reporting `GET /api/sites/{siteId}/activity` returns 30-day counts for visits, starts, completions, replays and shares to authorized workspace members. `POST /api/sites/public/{siteId}/activity` accepts `{id: EVENT_UUID, visitId: VISIT_UUID, event: "visit" | "start" | "complete" | "replay" | "share"}`. Reuse the event UUID when retrying. Use a site-scoped visit ID. Never send emails, profile data or arbitrary page contents to this endpoint. These are browser-reported interactions, not verified Spotify listening or a named fan activity history. Recoup-hosted generated sites use the trusted host bridge: `window.recoup.track(event)` reports approved events; `window.recoup.join()` reveals the trusted signup controls. External sites should implement the equivalent fixed-event transport and keep account credentials on the server. ## Playback for Premium and Free listeners The hosted player reads the connected Spotify profile's `product` field. Premium listeners use the Spotify Web Playback SDK. Free (`free` or `open`) listeners use the site's saved, verified recording through the same play/pause, seek and volume controls. If the profile cannot be read, its subscription remains unknown; available artist audio keeps playback usable. The player also handles a Spotify SDK account rejection by switching to the recording. `GET /api/sites/public/{siteId}` includes `playbackAudioUrl`, a fresh signed audio URL valid for one hour, or `null` when no matching verified recording is available. It is resolved from the published site's workspace-owned context request and matching Spotify track; private research and analysis are excluded. Fetch a fresh public response when reloading rather than storing this URL permanently. Publish only audio you have permission to serve to fans. File playback is not a Spotify stream. Spotify playback subscription, Recoup's paid fan-connection entitlement, and fan capture are separate concerns. A missing audio file must show an honest unavailable state while keeping the game playable. # One player for every release Source: https://recoupable.dev/docs/essentials/release-player Create a listening page or embed Spotify and Apple Music playback on your artist website. Create a player for a song, album or Spotify playlist. Share its listening page or embed it on your website. Recoup handles music-service sign-in, supported browser playback and fan activity reporting. ## Create and publish Use the [Recoup API](https://recoupable.dev/docs/api-reference/players/create) or the `create_release_player` MCP tool. Choose an artist you can access, name the player, and provide a `spotifyUrl`, an `appleUrl`, or both. Use `organizationId` to create it in your organization workspace. For website embeds, add the exact website origins to `allowedOrigins`. An origin includes the scheme, hostname and any non-default port, such as `https://artist.example:8443`. Embed origins must use HTTPS. Include both the www and non-www origins if fans can visit either. You can omit this field when sharing only the hosted listening page. Example create request: ```json { "artistId": "ARTIST_ACCOUNT_UUID", "organizationId": "ORGANIZATION_UUID", "name": "Release title", "spotifyUrl": "https://open.spotify.com/album/ALBUM_ID", "appleUrl": "https://music.apple.com/us/album/release/APPLE_ID", "allowedOrigins": ["https://artist.example", "https://www.artist.example"] } ``` New players start unpublished. Review the music links, then publish with `update_release_player`, passing `id`, the current `revision`, and `enabled: true`. Publishing requires an active paid Recoup workspace. Use the same tool to change the music links, title, artwork or approved website origins. Pass the current revision returned by Recoup with each update. The player stays assigned to its original artist and workspace. Updating it ends existing player sessions, so fans may need to reconnect. See [update a release player](https://recoupable.dev/docs/api-reference/players/update) for the REST request and fields. ## Share a listening page Use the returned `listenUrl` as your bio or campaign link. No custom website is needed. Fans choose from the music services configured for that player. To track campaign traffic, add `source`, `medium`, `campaign`, and `content` query parameters. The listening page also accepts `utm_source`, `utm_medium`, `utm_campaign`, and `utm_content`. Use campaign labels, such as `instagram` and `release`; keep personal information out of links. ## Embed on your website Use the returned `spotifyEmbedUrl` or `appleEmbedUrl` for each configured service. The `parent` parameter must match an approved website origin. ```html ``` Keep music-service sign-in inside the Recoup player. Your website does not need Spotify or Apple credentials. Include a direct music-service link as a fallback when browser playback is unavailable. For Spotify Free handoffs, the embed sends a `recoup:open-dsp` message with `provider: "spotify"`. Your website must verify that the message comes from the Recoup player origin and that iframe's window, then open the Spotify destination configured for the player. Do not use a destination URL supplied by a message. ## What fans can play Spotify Premium fans can stream through the browser player after connecting. Spotify playlist players show a selectable song list and highlight the current song. Selecting a song starts playback at its position in the playlist. Unavailable entries cannot be selected. If the list fails to load, fans can retry or open the playlist in Spotify. Spotify Free fans are sent to the configured song, album or playlist in Spotify after connecting. Spotify controls whether that opens in its app or website and which playback options the fan has. If Recoup cannot confirm the account's subscription, it does not assume the account is Free. Apple Music playback uses the fan's Apple Music authorization and subscription. Apple sessions do not capture email or create an identified fan contact in Recoup. ## Optional uploaded audio for Spotify Free fans You can choose to let Spotify Free fans play an uploaded audio file in Recoup instead of sending them to Spotify. Set `freePlayback: "audio"` and provide `audioUrl`. This setting belongs to the player owner; fans cannot change it. Premium fans continue to stream from Spotify. Upload an MP3 or WAV through `POST /api/sites/assets` in the same workspace, then use the returned `asset.url`. The current upload limit is 4 MB. Audio mode requires a Spotify destination as a fallback. MCP arguments for `update_release_player`: ```json { "id": "PLAYER_UUID", "revision": 1, "freePlayback": "audio", "audioUrl": "WORKSPACE_UPLOADED_ASSET_URL" } ``` For REST, send `PATCH /api/players/PLAYER_UUID` with the same body except `id`; the player ID belongs in the URL. Audio mode plays one uploaded file per player. It does not support an uploaded playlist or generate Spotify streams. Uploaded-file playback is excluded from the player's music-service playback reports; fan connections are still recorded. If the file is missing, fans are sent to Spotify. If it fails to load, the player offers an Open in Spotify link. To restore the Spotify handoff, set `freePlayback: "spotify"`. You can also clear `audioUrl` by setting it to `null`. ## Fan contacts belong to your workspace Spotify sign-in captures the profile information and email that Spotify makes available. A returning Spotify account reuses its contact in the same workspace. If that fan connects to another artist in your workspace, Recoup adds an artist relationship to the same contact. If they connect in another organization, that organization gets a separate contact. Your contact and listening history stay in your workspace. Matching names or email addresses alone do not merge fans. - Use `get_workspace_player_fans` or [read workspace player fans](https://recoupable.dev/docs/api-reference/players/workspace-fans) to list contacts and their artist relationships. - Use `get_release_player_fans` or [read artist player fans](https://recoupable.dev/docs/api-reference/players/fans) to list fan relationships across that artist's players in the workspace. `contact_id` identifies the workspace contact. Each artist relationship has its own `id`; activity records refer to that relationship as `fan_id`. Spotify sign-in does **not** grant email marketing consent. Use a separate, optional signup before sending marketing emails. Apple Music sessions remain anonymous and are not linked to Spotify contacts. ## Measure listening activity Use `get_release_player_activity` or [read listening activity](https://recoupable.dev/docs/api-reference/players/activity) for 30-day campaign totals and individual track, play, pause and skip events. These reports describe activity observed while the Recoup player is open. Playback after a fan leaves for Spotify or Apple Music is outside these reports. Player events and reported listening time are **not confirmed music-service stream counts** and do not prove that a campaign caused additional streams. Compare them with catalog stream measurements to evaluate campaign performance. ## Check your fan journey Before promoting the link, test it on the phones and social browsers your fans use. Confirm that sign-in works, the correct music opens, playlist songs can be selected, and the Spotify Free fallback works. Then check fan and activity reports in the intended workspace. # Create a release player Source: https://recoupable.dev/docs/api-reference/players/create Create a release player ## POST /api/players Full OpenAPI specification: https://recoupable.dev/docs/spec/players.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Release Players", "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/players": { "post": { "summary": "Create a release player", "description": "Register one reusable listening page plus provider embeds. Returns player, listenUrl, spotifyEmbedUrl, appleEmbedUrl. MCP: create_release_player. No site generation is required. This feature requires its API/app/database releases.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [], "responses": { "201": { "description": "PlayerResource fields returned by the release player service.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerResource" } } } }, "400": { "description": "Invalid input", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "401": { "description": "Missing/invalid authentication or failed provider authorization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "402": { "description": "Publishing requires an active paid workspace", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "403": { "description": "Workspace, origin, or session access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "404": { "description": "Player not available", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "429": { "description": "Request rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "503": { "description": "Feature/configuration temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } } }, "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": false, "required": [ "artistId", "name" ], "properties": { "organizationId": { "type": [ "string", "null" ], "format": "uuid", "description": "Authorized workspace; omit for the authenticated account. Never send account_id." }, "artistId": { "type": "string", "format": "uuid", "description": "Artist account belonging to this workspace." }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "spotifyUrl": { "type": [ "string", "null" ], "format": "uri", "description": "Spotify track, album, or playlist URL. Share parameters are removed." }, "appleUrl": { "type": [ "string", "null" ], "format": "uri", "description": "Apple Music song or album URL, optionally an album ?i= song selection." }, "artwork": { "type": [ "string", "null" ], "format": "uri", "description": "Optional public HTTPS artwork." }, "allowedOrigins": { "type": "array", "maxItems": 10, "items": { "type": "string", "format": "uri" }, "description": "Exact HTTPS origins authorized to embed this player; no path, query, credentials, or fragment." }, "enabled": { "type": "boolean", "default": false, "description": "Publishing requires an active paid workspace. Disabled players are unavailable publicly." }, "freePlayback": { "type": "string", "enum": [ "spotify", "audio" ], "default": "spotify", "description": "Release-owner choice for verified Spotify Free accounts. spotify opens the configured Spotify release; audio plays a workspace-owned uploaded file. Premium continues using Spotify streaming." }, "audioUrl": { "type": [ "string", "null" ], "format": "uri", "description": "MP3 or WAV URL returned by POST /api/sites/assets for the same workspace. Requires an existing audio object; arbitrary or other-workspace URLs are rejected. Required when freePlayback is audio. Uploads currently have a 4 MB limit." } }, "description": "At least one of spotifyUrl or appleUrl is required. Disabled by default.", "anyOf": [ { "required": [ "spotifyUrl" ], "properties": { "spotifyUrl": { "type": "string", "format": "uri" } } }, { "required": [ "appleUrl" ], "properties": { "appleUrl": { "type": "string", "format": "uri" } } } ] } } } } } } }, "components": { "schemas": { "PlayerResource": { "type": "object", "properties": { "player": { "$ref": "#/components/schemas/ReleasePlayer" }, "listenUrl": { "type": "string", "format": "uri" }, "spotifyEmbedUrl": { "type": "string", "format": "uri", "description": "Stable provider route. This URL is returned even when that provider is unconfigured; inspect the corresponding destination in player before displaying it." }, "appleEmbedUrl": { "type": "string", "format": "uri", "description": "Stable provider route. This URL is returned even when that provider is unconfigured; inspect the corresponding destination in player before displaying it." } }, "required": [ "player", "listenUrl", "spotifyEmbedUrl", "appleEmbedUrl" ] }, "PlayerError": { "type": "object", "properties": { "error": { "type": "string" }, "status": { "type": "string" } }, "required": [ "error" ] }, "ReleasePlayer": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "owner_id": { "type": "string", "format": "uuid" }, "artist_id": { "type": "string", "format": "uuid" }, "created_by": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "spotify_url": { "type": [ "string", "null" ], "format": "uri" }, "apple_url": { "type": [ "string", "null" ], "format": "uri" }, "artwork": { "type": [ "string", "null" ], "format": "uri" }, "allowed_origins": { "type": "array", "items": { "type": "string", "format": "uri" } }, "enabled": { "type": "boolean" }, "revision": { "type": "integer", "minimum": 1 }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" }, "free_playback": { "type": "string", "enum": [ "spotify", "audio" ], "default": "spotify", "description": "Release-owner choice for verified Spotify Free accounts. spotify opens the configured Spotify release; audio plays a workspace-owned uploaded file. Premium continues using Spotify streaming." }, "audio_url": { "type": [ "string", "null" ], "format": "uri", "description": "MP3 or WAV URL returned by POST /api/sites/assets for the same workspace. Requires an existing audio object; arbitrary or other-workspace URLs are rejected. Required when freePlayback is audio. Uploads currently have a 4 MB limit." } }, "required": [ "id", "owner_id", "artist_id", "name", "spotify_url", "apple_url", "allowed_origins", "enabled", "revision", "free_playback", "audio_url" ] } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # List release players Source: https://recoupable.dev/docs/api-reference/players/list List release players ## GET /api/players Full OpenAPI specification: https://recoupable.dev/docs/spec/players.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Release Players", "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/players": { "get": { "summary": "List release players", "description": "List up to 100 players in the authenticated workspace. MCP: list_release_player.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "organizationId", "in": "query", "schema": { "type": [ "string", "null" ], "format": "uuid", "description": "Authorized workspace; omit for the authenticated account. Never send account_id." } }, { "name": "offset", "in": "query", "schema": { "type": "integer", "minimum": 0, "maximum": 100000, "default": 0 } }, { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } } ], "responses": { "200": { "description": "PlayerList fields returned by the release player service.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerList" } } } }, "400": { "description": "Invalid input", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "401": { "description": "Missing/invalid authentication or failed provider authorization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "403": { "description": "Workspace, origin, or session access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "404": { "description": "Player not available", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "429": { "description": "Request rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "503": { "description": "Feature/configuration temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } } } } } }, "components": { "schemas": { "PlayerList": { "type": "object", "properties": { "players": { "type": "array", "items": { "$ref": "#/components/schemas/ReleasePlayer" } }, "offset": { "type": "integer" }, "limit": { "type": "integer" }, "nextOffset": { "type": [ "integer", "null" ], "description": "Use as offset for the next page; null marks the end." } }, "required": [ "players" ] }, "PlayerError": { "type": "object", "properties": { "error": { "type": "string" }, "status": { "type": "string" } }, "required": [ "error" ] }, "ReleasePlayer": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "owner_id": { "type": "string", "format": "uuid" }, "artist_id": { "type": "string", "format": "uuid" }, "created_by": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "spotify_url": { "type": [ "string", "null" ], "format": "uri" }, "apple_url": { "type": [ "string", "null" ], "format": "uri" }, "artwork": { "type": [ "string", "null" ], "format": "uri" }, "allowed_origins": { "type": "array", "items": { "type": "string", "format": "uri" } }, "enabled": { "type": "boolean" }, "revision": { "type": "integer", "minimum": 1 }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" }, "free_playback": { "type": "string", "enum": [ "spotify", "audio" ], "default": "spotify", "description": "Release-owner choice for verified Spotify Free accounts. spotify opens the configured Spotify release; audio plays a workspace-owned uploaded file. Premium continues using Spotify streaming." }, "audio_url": { "type": [ "string", "null" ], "format": "uri", "description": "MP3 or WAV URL returned by POST /api/sites/assets for the same workspace. Requires an existing audio object; arbitrary or other-workspace URLs are rejected. Required when freePlayback is audio. Uploads currently have a 4 MB limit." } }, "required": [ "id", "owner_id", "artist_id", "name", "spotify_url", "apple_url", "allowed_origins", "enabled", "revision", "free_playback", "audio_url" ] } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Read a release player Source: https://recoupable.dev/docs/api-reference/players/get Read a release player ## GET /api/players/{id} Full OpenAPI specification: https://recoupable.dev/docs/spec/players.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Release Players", "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/players/{id}": { "get": { "summary": "Read a release player", "description": "Read settings, revision and share/embed links. MCP: get_release_player.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "organizationId", "in": "query", "schema": { "type": [ "string", "null" ], "format": "uuid", "description": "Authorized workspace; omit for the authenticated account. Never send account_id." } } ], "responses": { "200": { "description": "PlayerResource fields returned by the release player service.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerResource" } } } }, "400": { "description": "Invalid input", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "401": { "description": "Missing/invalid authentication or failed provider authorization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "403": { "description": "Workspace, origin, or session access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "404": { "description": "Player not available", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "429": { "description": "Request rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "503": { "description": "Feature/configuration temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } } } } } }, "components": { "schemas": { "PlayerResource": { "type": "object", "properties": { "player": { "$ref": "#/components/schemas/ReleasePlayer" }, "listenUrl": { "type": "string", "format": "uri" }, "spotifyEmbedUrl": { "type": "string", "format": "uri", "description": "Stable provider route. This URL is returned even when that provider is unconfigured; inspect the corresponding destination in player before displaying it." }, "appleEmbedUrl": { "type": "string", "format": "uri", "description": "Stable provider route. This URL is returned even when that provider is unconfigured; inspect the corresponding destination in player before displaying it." } }, "required": [ "player", "listenUrl", "spotifyEmbedUrl", "appleEmbedUrl" ] }, "PlayerError": { "type": "object", "properties": { "error": { "type": "string" }, "status": { "type": "string" } }, "required": [ "error" ] }, "ReleasePlayer": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "owner_id": { "type": "string", "format": "uuid" }, "artist_id": { "type": "string", "format": "uuid" }, "created_by": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "spotify_url": { "type": [ "string", "null" ], "format": "uri" }, "apple_url": { "type": [ "string", "null" ], "format": "uri" }, "artwork": { "type": [ "string", "null" ], "format": "uri" }, "allowed_origins": { "type": "array", "items": { "type": "string", "format": "uri" } }, "enabled": { "type": "boolean" }, "revision": { "type": "integer", "minimum": 1 }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" }, "free_playback": { "type": "string", "enum": [ "spotify", "audio" ], "default": "spotify", "description": "Release-owner choice for verified Spotify Free accounts. spotify opens the configured Spotify release; audio plays a workspace-owned uploaded file. Premium continues using Spotify streaming." }, "audio_url": { "type": [ "string", "null" ], "format": "uri", "description": "MP3 or WAV URL returned by POST /api/sites/assets for the same workspace. Requires an existing audio object; arbitrary or other-workspace URLs are rejected. Required when freePlayback is audio. Uploads currently have a 4 MB limit." } }, "required": [ "id", "owner_id", "artist_id", "name", "spotify_url", "apple_url", "allowed_origins", "enabled", "revision", "free_playback", "audio_url" ] } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Update a release player Source: https://recoupable.dev/docs/api-reference/players/update Update a release player ## PATCH /api/players/{id} Full OpenAPI specification: https://recoupable.dev/docs/spec/players.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Release Players", "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/players/{id}": { "patch": { "summary": "Update a release player", "description": "Edit branding, DSP destinations, embed origins, or enabled. Requires current revision; stale edits return 409. Artist/owner cannot change. Any update invalidates existing listening sessions. MCP: update_release_player.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "PlayerResource fields returned by the release player service.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerResource" } } } }, "400": { "description": "Invalid input", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "401": { "description": "Missing/invalid authentication or failed provider authorization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "402": { "description": "Publishing requires an active paid workspace", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "403": { "description": "Workspace, origin, or session access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "404": { "description": "Player not available", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "409": { "description": "The player revision changed. Read the latest settings before retrying.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "429": { "description": "Request rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "503": { "description": "Feature/configuration temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } } }, "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": false, "required": [ "revision" ], "properties": { "revision": { "type": "integer", "minimum": 1 }, "organizationId": { "type": [ "string", "null" ], "format": "uuid", "description": "Authorized workspace; omit for the authenticated account. Never send account_id." }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "spotifyUrl": { "type": [ "string", "null" ], "format": "uri", "description": "Spotify track, album, or playlist URL. Share parameters are removed." }, "appleUrl": { "type": [ "string", "null" ], "format": "uri", "description": "Apple Music song or album URL, optionally an album ?i= song selection." }, "artwork": { "type": [ "string", "null" ], "format": "uri", "description": "Optional public HTTPS artwork." }, "allowedOrigins": { "type": "array", "maxItems": 10, "items": { "type": "string", "format": "uri" }, "description": "Exact HTTPS origins authorized to embed this player; no path, query, credentials, or fragment." }, "enabled": { "type": "boolean", "default": false, "description": "Publishing requires an active paid workspace. Disabled players are unavailable publicly." }, "freePlayback": { "type": "string", "enum": [ "spotify", "audio" ], "description": "Release-owner choice for verified Spotify Free accounts. spotify opens the configured Spotify release; audio plays a workspace-owned uploaded file. Premium continues using Spotify streaming." }, "audioUrl": { "type": [ "string", "null" ], "format": "uri", "description": "MP3 or WAV URL returned by POST /api/sites/assets for the same workspace. Requires an existing audio object; arbitrary or other-workspace URLs are rejected. Required when freePlayback is audio. Uploads currently have a 4 MB limit." } } } } } } } } }, "components": { "schemas": { "PlayerResource": { "type": "object", "properties": { "player": { "$ref": "#/components/schemas/ReleasePlayer" }, "listenUrl": { "type": "string", "format": "uri" }, "spotifyEmbedUrl": { "type": "string", "format": "uri", "description": "Stable provider route. This URL is returned even when that provider is unconfigured; inspect the corresponding destination in player before displaying it." }, "appleEmbedUrl": { "type": "string", "format": "uri", "description": "Stable provider route. This URL is returned even when that provider is unconfigured; inspect the corresponding destination in player before displaying it." } }, "required": [ "player", "listenUrl", "spotifyEmbedUrl", "appleEmbedUrl" ] }, "PlayerError": { "type": "object", "properties": { "error": { "type": "string" }, "status": { "type": "string" } }, "required": [ "error" ] }, "ReleasePlayer": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "owner_id": { "type": "string", "format": "uuid" }, "artist_id": { "type": "string", "format": "uuid" }, "created_by": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "spotify_url": { "type": [ "string", "null" ], "format": "uri" }, "apple_url": { "type": [ "string", "null" ], "format": "uri" }, "artwork": { "type": [ "string", "null" ], "format": "uri" }, "allowed_origins": { "type": "array", "items": { "type": "string", "format": "uri" } }, "enabled": { "type": "boolean" }, "revision": { "type": "integer", "minimum": 1 }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" }, "free_playback": { "type": "string", "enum": [ "spotify", "audio" ], "default": "spotify", "description": "Release-owner choice for verified Spotify Free accounts. spotify opens the configured Spotify release; audio plays a workspace-owned uploaded file. Premium continues using Spotify streaming." }, "audio_url": { "type": [ "string", "null" ], "format": "uri", "description": "MP3 or WAV URL returned by POST /api/sites/assets for the same workspace. Requires an existing audio object; arbitrary or other-workspace URLs are rejected. Required when freePlayback is audio. Uploads currently have a 4 MB limit." } }, "required": [ "id", "owner_id", "artist_id", "name", "spotify_url", "apple_url", "allowed_origins", "enabled", "revision", "free_playback", "audio_url" ] } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Read listening activity Source: https://recoupable.dev/docs/api-reference/players/activity Read player listening activity ## GET /api/players/{id}/activity Full OpenAPI specification: https://recoupable.dev/docs/spec/players.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Release Players", "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/players/{id}/activity": { "get": { "summary": "Read player listening activity", "description": "Private 30-day summary plus up to 100 events, newest first. Returns measurement=browser_reported_playback, dspStreams=null, periodDays, offset, limit and report (sessions, connectedFans, reportedListeningMs, playEvents, campaigns, activity). Activity has session_id, provider, track_id, event, position_ms, listened_ms, received_at, fan_id and display_name. Anonymous Apple sessions have no fan identity. These are reported SDK observations, not DSP stream counts, cross-device monitoring, or causal uplift. MCP: get_release_player_activity.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "organizationId", "in": "query", "schema": { "type": [ "string", "null" ], "format": "uuid", "description": "Authorized workspace; omit for the authenticated account. Never send account_id." } }, { "name": "offset", "in": "query", "schema": { "type": "integer", "minimum": 0, "maximum": 100000, "default": 0 } } ], "responses": { "200": { "description": "PlayerReportResponse fields returned by the release player service.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerReportResponse" } } } }, "400": { "description": "Invalid input", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "401": { "description": "Missing/invalid authentication or failed provider authorization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "403": { "description": "Workspace, origin, or session access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "404": { "description": "Player not available", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "429": { "description": "Request rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "503": { "description": "Feature/configuration temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } } } } } }, "components": { "schemas": { "PlayerReportResponse": { "type": "object", "properties": { "measurement": { "type": "string", "const": "browser_reported_playback" }, "dspStreams": { "type": "null" }, "periodDays": { "type": "integer", "const": 30 }, "offset": { "type": "integer", "format": "int64" }, "limit": { "type": "integer", "const": 100 }, "report": { "type": "object", "properties": { "sessions": { "type": "integer", "format": "int64" }, "connectedFans": { "type": "integer", "format": "int64" }, "reportedListeningMs": { "type": "integer", "format": "int64" }, "playEvents": { "type": "integer", "format": "int64" }, "campaigns": { "type": "array", "items": { "type": "object", "properties": { "provider": { "type": "string" }, "source": { "type": [ "string", "null" ] }, "campaign": { "type": [ "string", "null" ] }, "sessions": { "type": "integer", "format": "int64" }, "listened_ms": { "type": "integer", "format": "int64" } }, "required": [] } }, "activity": { "type": "array", "items": { "$ref": "#/components/schemas/PlayerActivity" } } }, "required": [ "sessions", "connectedFans", "reportedListeningMs", "playEvents", "campaigns", "activity" ] } }, "required": [ "measurement", "dspStreams", "periodDays", "offset", "limit", "report" ], "description": "Reporting envelope uses camelCase. Nested activity and campaign rows intentionally expose raw database snake_case fields." }, "PlayerError": { "type": "object", "properties": { "error": { "type": "string" }, "status": { "type": "string" } }, "required": [ "error" ] }, "PlayerActivity": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "session_id": { "type": "string", "format": "uuid" }, "event": { "type": "string" }, "provider": { "type": "string", "enum": [ "spotify", "apple_music" ] }, "track_id": { "type": [ "string", "null" ] }, "position_ms": { "type": "integer", "format": "int64" }, "listened_ms": { "type": "integer", "format": "int64" }, "received_at": { "type": "string", "format": "date-time" }, "fan_id": { "type": [ "string", "null" ], "format": "uuid" }, "display_name": { "type": [ "string", "null" ] }, "contact_id": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "id", "session_id", "event", "provider", "listened_ms", "received_at" ], "description": "Raw database reporting row; snake_case keys are preserved intentionally." } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Read artist player fans Source: https://recoupable.dev/docs/api-reference/players/fans Read artist player fans ## GET /api/players/{id}/fans Full OpenAPI specification: https://recoupable.dev/docs/spec/players.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Release Players", "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/players/{id}/fans": { "get": { "summary": "Read artist player fans", "description": "Artist-level Spotify fan relationships across releases in the authorized workspace. Each row keeps its stable relationship id and adds contact_id, the workspace-owned contact shared across artist relationships. Email/display_name come from the latest available confirmed workspace contact profile. scope=artist_in_workspace; marketingConsent=false. No provider tokens or cross-workspace identity links. MCP: get_release_player_fans.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "organizationId", "in": "query", "schema": { "type": [ "string", "null" ], "format": "uuid", "description": "Authorized workspace; omit for the authenticated account. Never send account_id." } }, { "name": "offset", "in": "query", "schema": { "type": "integer", "minimum": 0, "maximum": 100000, "default": 0 } }, { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } } ], "responses": { "200": { "description": "PlayerFansResponse fields returned by the release player service.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerFansResponse" } } } }, "400": { "description": "Invalid input", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "401": { "description": "Missing/invalid authentication or failed provider authorization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "403": { "description": "Workspace, origin, or session access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "404": { "description": "Player not available", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "429": { "description": "Request rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "503": { "description": "Feature/configuration temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } } } } } }, "components": { "schemas": { "PlayerFansResponse": { "type": "object", "properties": { "fans": { "type": "array", "items": { "$ref": "#/components/schemas/PlayerFan" } }, "scope": { "type": "string", "const": "artist_in_workspace" }, "marketingConsent": { "type": "boolean", "const": false }, "offset": { "type": "integer", "format": "int64" }, "limit": { "type": "integer", "format": "int64" } }, "required": [ "fans", "scope", "marketingConsent", "offset", "limit" ] }, "PlayerError": { "type": "object", "properties": { "error": { "type": "string" }, "status": { "type": "string" } }, "required": [ "error" ] }, "PlayerFan": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "provider": { "type": "string", "const": "spotify" }, "email": { "type": [ "string", "null" ] }, "display_name": { "type": [ "string", "null" ] }, "first_connected_at": { "type": "string", "format": "date-time" }, "last_connected_at": { "type": "string", "format": "date-time" }, "contact_id": { "type": "string", "format": "uuid", "description": "Workspace-owned contact shared across this fan’s artist relationships." } }, "required": [ "id", "provider", "first_connected_at", "last_connected_at", "contact_id" ] } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Read workspace player fans Source: https://recoupable.dev/docs/api-reference/players/workspace-fans Read workspace player fans ## GET /api/players/fans Full OpenAPI specification: https://recoupable.dev/docs/spec/players.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Release Players", "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/players/fans": { "get": { "summary": "Read workspace player fans", "description": "One contact per workspace and Spotify provider identity, with all artist relationships for that workspace. Authentication and current workspace access are required. Never merged by email/name or linked across organizations. Sign-in does not grant email marketing consent. MCP: get_workspace_player_fans.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "parameters": [ { "name": "organizationId", "in": "query", "schema": { "type": [ "string", "null" ], "format": "uuid", "description": "Authorized workspace; omit for the authenticated account. Never send account_id." } }, { "name": "offset", "in": "query", "schema": { "type": "integer", "minimum": 0, "maximum": 100000, "default": 0 } }, { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } } ], "responses": { "200": { "description": "PlayerFansResponse fields returned by the release player service.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WorkspacePlayerFansResponse" } } } }, "400": { "description": "Invalid input", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "401": { "description": "Missing/invalid authentication or failed provider authorization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "403": { "description": "Workspace, origin, or session access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "429": { "description": "Request rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "503": { "description": "Feature/configuration temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } } } } } }, "components": { "schemas": { "WorkspacePlayerFansResponse": { "type": "object", "required": [ "fans", "scope", "marketingConsent", "offset", "limit", "nextOffset" ], "properties": { "fans": { "type": "array", "items": { "type": "object", "required": [ "id", "provider", "email", "display_name", "first_connected_at", "last_connected_at", "artists" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Contact ID within this workspace, not an artist relationship ID." }, "provider": { "type": "string", "const": "spotify" }, "email": { "type": [ "string", "null" ] }, "display_name": { "type": [ "string", "null" ] }, "first_connected_at": { "type": "string", "format": "date-time" }, "last_connected_at": { "type": "string", "format": "date-time" }, "artists": { "type": "array", "items": { "type": "object", "required": [ "artist_id", "first_connected_at", "last_connected_at" ], "properties": { "artist_id": { "type": "string", "format": "uuid" }, "first_connected_at": { "type": "string", "format": "date-time" }, "last_connected_at": { "type": "string", "format": "date-time" } } } } } } }, "scope": { "type": "string", "const": "workspace" }, "marketingConsent": { "type": "boolean", "const": false }, "offset": { "type": "integer" }, "limit": { "type": "integer" }, "nextOffset": { "type": [ "integer", "null" ] } } }, "PlayerError": { "type": "object", "properties": { "error": { "type": "string" }, "status": { "type": "string" } }, "required": [ "error" ] } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Compose music Source: https://recoupable.dev/docs/api-reference/complete/content/post-music-compose **Availability: pending reconciliation.** This specialized music REST route has no matching route in the inspected API source. Existing skills or local composition workflows do not establish this hosted path. The supported music-generation API is a separate contract; the retained outcome and public route need reconciliation. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation. ## POST /api/music/compose Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/music/compose": { "post": { "description": "**Availability: pending reconciliation.** This specialized music REST route has no matching route in the inspected API source. Existing skills or local composition workflows do not establish this hosted path. The supported music-generation API is a separate contract; the retained outcome and public route need reconciliation. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation.\n\nGenerate a song from a text prompt or a composition plan. The prompt can describe mood, genre, instruments, lyrics, and structure. Alternatively, pass a full composition_plan for fine-grained control over sections, styles, and lyrics. Returns binary audio in the requested format (default MP3).", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Music composition parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicComposeRequest" } } } }, "responses": { "200": { "description": "Audio generated successfully", "headers": { "song-id": { "schema": { "type": "string" }, "description": "Unique identifier for the generated song" } }, "content": { "audio/mpeg": { "schema": { "type": "string", "format": "binary" } } } }, "400": { "description": "Validation failed — invalid or missing request body fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicErrorResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicErrorResponse" } } } } }, "x-recoup-availability": "pending" } } }, "components": { "schemas": { "MusicComposeRequest": { "type": "object", "description": "Parameters for generating a song. Provide either a text prompt or a composition_plan.", "properties": { "prompt": { "type": "string", "description": "Text prompt describing the desired song — mood, genre, instruments, lyrics, structure." }, "composition_plan": { "type": "object", "description": "Full composition plan for fine-grained control over sections, styles, and lyrics. Use POST /api/music/plan to generate one." }, "duration": { "type": "number", "description": "Desired duration in seconds.", "default": 30 }, "output_format": { "type": "string", "description": "Audio output format (e.g. mp3_44100_192).", "default": "mp3_44100_192" }, "sign_with_c2pa": { "type": "boolean", "description": "Sign the output with C2PA content credentials.", "default": false } } }, "MusicErrorResponse": { "type": "object", "required": [ "error" ], "description": "Error response from a music endpoint.", "properties": { "error": { "type": "string", "description": "Error message describing what went wrong." } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Compose Music with Metadata Source: https://recoupable.dev/docs/api-reference/complete/content/post-music-compose-detailed **Availability: pending reconciliation.** This specialized music REST route has no matching route in the inspected API source. Existing skills or local composition workflows do not establish this hosted path. The supported music-generation API is a separate contract; the retained outcome and public route need reconciliation. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation. ## POST /api/music/compose/detailed Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/music/compose/detailed": { "post": { "description": "**Availability: pending reconciliation.** This specialized music REST route has no matching route in the inspected API source. Existing skills or local composition workflows do not establish this hosted path. The supported music-generation API is a separate contract; the retained outcome and public route need reconciliation. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation.\n\nGenerate a song with metadata and optional word-level timestamps. Returns a multipart/mixed response: the first part is JSON metadata (song ID, duration, seed, etc.) and the second part is binary audio. Use with_timestamps to get precise timing for each word in the lyrics.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Music composition parameters with optional timestamps", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicComposeDetailedRequest" } } } }, "responses": { "200": { "description": "Audio and metadata generated successfully. Response is multipart/mixed: JSON metadata followed by binary audio.", "content": { "multipart/mixed": { "schema": { "type": "string", "format": "binary" } } } }, "400": { "description": "Validation failed — invalid or missing request body fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicErrorResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicErrorResponse" } } } } }, "x-recoup-availability": "pending" } } }, "components": { "schemas": { "MusicComposeDetailedRequest": { "type": "object", "description": "Parameters for generating a song with metadata and optional word-level timestamps.", "properties": { "prompt": { "type": "string", "description": "Text prompt describing the desired song — mood, genre, instruments, lyrics, structure." }, "composition_plan": { "type": "object", "description": "Full composition plan for fine-grained control over sections, styles, and lyrics. Use POST /api/music/plan to generate one." }, "duration": { "type": "number", "description": "Desired duration in seconds.", "default": 30 }, "output_format": { "type": "string", "description": "Audio output format (e.g. mp3_44100_192).", "default": "mp3_44100_192" }, "sign_with_c2pa": { "type": "boolean", "description": "Sign the output with C2PA content credentials.", "default": false }, "with_timestamps": { "type": "boolean", "description": "Include word-level timestamps in the metadata response.", "default": false } } }, "MusicErrorResponse": { "type": "object", "required": [ "error" ], "description": "Error response from a music endpoint.", "properties": { "error": { "type": "string", "description": "Error message describing what went wrong." } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Stream music Source: https://recoupable.dev/docs/api-reference/complete/content/post-music-stream **Availability: pending reconciliation.** This specialized music REST route has no matching route in the inspected API source. Existing skills or local composition workflows do not establish this hosted path. The supported music-generation API is a separate contract; the retained outcome and public route need reconciliation. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation. ## POST /api/music/stream Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/music/stream": { "post": { "description": "**Availability: pending reconciliation.** This specialized music REST route has no matching route in the inspected API source. Existing skills or local composition workflows do not establish this hosted path. The supported music-generation API is a separate contract; the retained outcome and public route need reconciliation. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation.\n\nGenerate a song and stream audio chunks in real time. Use this endpoint when you want to begin playback before the full song is generated. The response streams binary audio chunks as they are produced.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Music streaming parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicStreamRequest" } } } }, "responses": { "200": { "description": "Audio stream started successfully", "headers": { "song-id": { "schema": { "type": "string" }, "description": "Unique identifier for the generated song" } }, "content": { "audio/mpeg": { "schema": { "type": "string", "format": "binary" } } } }, "400": { "description": "Validation failed — invalid or missing request body fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicErrorResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicErrorResponse" } } } } }, "x-recoup-availability": "pending" } } }, "components": { "schemas": { "MusicStreamRequest": { "type": "object", "description": "Parameters for generating a song with real-time audio streaming.", "properties": { "prompt": { "type": "string", "description": "Text prompt describing the desired song — mood, genre, instruments, lyrics, structure." }, "composition_plan": { "type": "object", "description": "Full composition plan for fine-grained control over sections, styles, and lyrics. Use POST /api/music/plan to generate one." }, "duration": { "type": "number", "description": "Desired duration in seconds.", "default": 30 }, "output_format": { "type": "string", "description": "Audio output format (e.g. mp3_44100_192).", "default": "mp3_44100_192" }, "sign_with_c2pa": { "type": "boolean", "description": "Sign the output with C2PA content credentials.", "default": false } } }, "MusicErrorResponse": { "type": "object", "required": [ "error" ], "description": "Error response from a music endpoint.", "properties": { "error": { "type": "string", "description": "Error message describing what went wrong." } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Create a composition plan Source: https://recoupable.dev/docs/api-reference/complete/content/post-music-plan **Availability: pending reconciliation.** This specialized music REST route has no matching route in the inspected API source. Existing skills or local composition workflows do not establish this hosted path. The supported music-generation API is a separate contract; the retained outcome and public route need reconciliation. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation. ## POST /api/music/plan Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/music/plan": { "post": { "description": "**Availability: pending reconciliation.** This specialized music REST route has no matching route in the inspected API source. Existing skills or local composition workflows do not establish this hosted path. The supported music-generation API is a separate contract; the retained outcome and public route need reconciliation. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation.\n\nCreate a composition plan from a text prompt. A composition plan is a structured representation of a song — sections, styles, lyrics, and durations — that you can review and tweak before passing to the compose endpoint. This endpoint is free and does not consume credits.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Plan creation parameters", "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicCreatePlanRequest" } } } }, "responses": { "200": { "description": "Composition plan created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicCreatePlanResponse" } } } }, "400": { "description": "Validation failed — invalid or missing request body fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicErrorResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicErrorResponse" } } } } }, "x-recoup-availability": "pending" } } }, "components": { "schemas": { "MusicCreatePlanRequest": { "type": "object", "required": [ "prompt" ], "description": "Parameters for creating a composition plan from a text prompt.", "properties": { "prompt": { "type": "string", "description": "Text prompt describing the desired song." } } }, "MusicCreatePlanResponse": { "type": "object", "description": "A structured composition plan that can be reviewed, edited, and passed to the compose endpoint.", "properties": { "plan": { "type": "object", "description": "The composition plan with sections, styles, lyrics, and durations.", "properties": { "sections": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string", "description": "Section name (e.g. intro, verse, chorus)" }, "duration": { "type": "number", "description": "Section duration in seconds" }, "style": { "type": "string", "description": "Style description for this section" }, "lyrics": { "type": "string", "description": "Lyrics for this section" } } } } } } } }, "MusicErrorResponse": { "type": "object", "required": [ "error" ], "description": "Error response from a music endpoint.", "properties": { "error": { "type": "string", "description": "Error message describing what went wrong." } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Create music from video Source: https://recoupable.dev/docs/api-reference/complete/content/post-music-video-to-music **Availability: pending reconciliation.** This specialized music REST route has no matching route in the inspected API source. Existing skills or local composition workflows do not establish this hosted path. The supported music-generation API is a separate contract; the retained outcome and public route need reconciliation. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation. ## POST /api/music/video-to-music Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/music/video-to-music": { "post": { "description": "**Availability: pending reconciliation.** This specialized music REST route has no matching route in the inspected API source. Existing skills or local composition workflows do not establish this hosted path. The supported music-generation API is a separate contract; the retained outcome and public route need reconciliation. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation.\n\nGenerate background music from video files. Upload 1–10 video files via multipart/form-data (max 200 MB total). The AI analyzes the video content and generates music that matches the mood, pacing, and style. Optionally provide a text description and style tags to guide the output.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Video files and optional generation parameters", "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "properties": { "files": { "type": "array", "items": { "type": "string", "format": "binary" }, "minItems": 1, "maxItems": 10, "description": "Video files to analyze (max 200 MB total)" }, "description": { "type": "string", "maxLength": 1000, "description": "Optional text description to guide the music generation" }, "tags": { "type": "array", "items": { "type": "string" }, "maxItems": 10, "description": "Optional style tags to influence the output (max 10)" }, "sign_with_c2pa": { "type": "boolean", "default": false, "description": "Sign the output with C2PA content credentials" }, "output_format": { "type": "string", "description": "Audio output format (e.g. mp3_44100_192)" } }, "required": [ "files" ] } } } }, "responses": { "200": { "description": "Background music generated successfully", "headers": { "song-id": { "schema": { "type": "string" }, "description": "Unique identifier for the generated song" } }, "content": { "audio/mpeg": { "schema": { "type": "string", "format": "binary" } } } }, "400": { "description": "Validation failed — invalid or missing request body fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicErrorResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicErrorResponse" } } } } }, "x-recoup-availability": "pending" } } }, "components": { "schemas": { "MusicErrorResponse": { "type": "object", "required": [ "error" ], "description": "Error response from a music endpoint.", "properties": { "error": { "type": "string", "description": "Error message describing what went wrong." } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Separate audio stems Source: https://recoupable.dev/docs/api-reference/complete/content/post-music-stem-separation **Availability: pending reconciliation.** This specialized music REST route has no matching route in the inspected API source. Existing skills or local composition workflows do not establish this hosted path. The supported music-generation API is a separate contract; the retained outcome and public route need reconciliation. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation. ## POST /api/music/stem-separation Full OpenAPI specification: https://recoupable.dev/docs/spec/content.json ## Authentication This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Content", "description": "API documentation for the Recoup platform - an AI agent platform for the music industry", "license": { "name": "MIT" }, "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/music/stem-separation": { "post": { "description": "**Availability: pending reconciliation.** This specialized music REST route has no matching route in the inspected API source. Existing skills or local composition workflows do not establish this hosted path. The supported music-generation API is a separate contract; the retained outcome and public route need reconciliation. Source baseline: October 12, 2026. Do not rely on this documented path as a currently supported hosted operation.\n\nSeparate an audio file into individual stems (vocals, drums, bass, etc.). Upload an audio file via multipart/form-data. Choose between two-stem separation (vocals + instrumental) or six-stem separation (vocals, drums, bass, guitar, piano, other). Returns a ZIP archive containing the separated stems.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "description": "Audio file and separation parameters", "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "properties": { "file": { "type": "string", "format": "binary", "description": "Audio file to separate into stems" }, "stem_variation_id": { "type": "string", "enum": [ "two_stems_v1", "six_stems_v1" ], "default": "six_stems_v1", "description": "Stem separation mode. `two_stems_v1` produces vocals + instrumental. `six_stems_v1` produces vocals, drums, bass, guitar, piano, and other." }, "sign_with_c2pa": { "type": "boolean", "default": false, "description": "Sign the output with C2PA content credentials" }, "output_format": { "type": "string", "description": "Audio output format (e.g. mp3_44100_192)" } }, "required": [ "file" ] } } } }, "responses": { "200": { "description": "Stems separated successfully. Returns a ZIP archive containing the individual stems.", "content": { "application/zip": { "schema": { "type": "string", "format": "binary" } } } }, "400": { "description": "Validation failed — invalid or missing request body fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicErrorResponse" } } } }, "401": { "description": "Unauthorized — invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MusicErrorResponse" } } } } }, "x-recoup-availability": "pending" } } }, "components": { "schemas": { "MusicErrorResponse": { "type": "object", "required": [ "error" ], "description": "Error response from a music endpoint.", "properties": { "error": { "type": "string", "description": "Error message describing what went wrong." } } } }, "securitySchemes": { "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key", "description": "Your Recoup API key. [Learn more](/docs/quickstart#api-keys)." }, "bearerAuth": { "type": "http", "scheme": "bearer" } } } } ``` # Read public player metadata without creating a session Source: https://recoupable.dev/docs/api-reference/complete/players/get-players-public-id Internal transport for the trusted Recoup browser player. Without provider, returns only public release settings. With provider, validates parent and creates or resumes a signed, provider-bound listening session. No owner, fan profile or email is exposed. The embedding website should use returned embed URLs rather than calling this API or handling credentials. ## GET /api/players/public/{id} Full OpenAPI specification: https://recoupable.dev/docs/spec/players.json ## Authentication This operation's specification permits a request without authentication. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Release Players", "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/players/public/{id}": { "get": { "summary": "Read public player metadata without creating a session", "description": "Internal transport for the trusted Recoup browser player. Without provider, returns only public release settings. With provider, validates parent and creates or resumes a signed, provider-bound listening session. No owner, fan profile or email is exposed. The embedding website should use returned embed URLs rather than calling this API or handling credentials.", "security": [], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "PublicPlayerConfig fields returned by the release player service.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicPlayerConfig" } } } }, "400": { "description": "Invalid input", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "401": { "description": "Missing/invalid authentication or failed provider authorization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "403": { "description": "Workspace, origin, or session access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "404": { "description": "Player not available", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "429": { "description": "Request rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "503": { "description": "Feature/configuration temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } } } } } }, "components": { "schemas": { "PublicPlayerConfig": { "type": "object", "properties": { "playerId": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "artwork": { "type": [ "string", "null" ], "format": "uri" }, "spotifyUrl": { "type": [ "string", "null" ], "format": "uri" }, "appleUrl": { "type": [ "string", "null" ], "format": "uri" }, "revision": { "type": "integer", "minimum": 1 }, "provider": { "type": "string", "enum": [ "spotify", "apple_music" ] }, "release": { "type": "string", "format": "uri" }, "sessionId": { "type": "string", "format": "uuid" }, "flow": { "type": "string", "description": "Short-lived signed player capability; only the trusted Recoup player should handle this." }, "spotify": { "type": "object", "properties": { "configured": { "type": "boolean" }, "clientId": { "type": [ "string", "null" ] }, "redirectUri": { "type": "string", "format": "uri" }, "scopes": { "type": "array", "items": { "type": "string" } } }, "required": [ "configured", "clientId", "redirectUri", "scopes" ] }, "freePlayback": { "type": "string", "enum": [ "spotify", "audio" ], "default": "spotify", "description": "Release-owner choice for verified Spotify Free accounts. spotify opens the configured Spotify release; audio plays a workspace-owned uploaded file. Premium continues using Spotify streaming." }, "audioUrl": { "type": [ "string", "null" ], "format": "uri", "description": "Included only in provider-session responses. Configured uploaded audio is provided for Spotify sessions in audio mode; otherwise null. Not included in chooser metadata." } }, "required": [ "playerId", "name", "spotifyUrl", "appleUrl", "revision", "freePlayback" ] }, "PlayerError": { "type": "object", "properties": { "error": { "type": "string" }, "status": { "type": "string" } }, "required": [ "error" ] } } } } ``` # Record reported listening Source: https://recoupable.dev/docs/api-reference/complete/players/post-players-events Internal trusted Recoup player transport. Requires its Origin plus signed flow; browser-facing same-origin app proxy forwards to API. Provider and fan attribution come from the stored session. Event UUID makes retries idempotent, listenedMs is capped at 30 seconds and checked against elapsed server time. Recording failures do not stop music. ## POST /api/players/events Full OpenAPI specification: https://recoupable.dev/docs/spec/players.json ## Authentication This operation's specification permits a request without authentication. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Release Players", "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/players/events": { "post": { "summary": "Record reported listening", "description": "Internal trusted Recoup player transport. Requires its Origin plus signed flow; browser-facing same-origin app proxy forwards to API. Provider and fan attribution come from the stored session. Event UUID makes retries idempotent, listenedMs is capped at 30 seconds and checked against elapsed server time. Recording failures do not stop music.", "security": [], "parameters": [], "responses": { "200": { "description": "ListeningReceipt fields returned by the release player service.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ListeningReceipt" } } } }, "400": { "description": "Invalid input", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "401": { "description": "Missing/invalid authentication or failed provider authorization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "403": { "description": "Workspace, origin, or session access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "404": { "description": "Player not available", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "429": { "description": "Request rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "503": { "description": "Feature/configuration temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } } }, "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": false, "required": [ "flow", "event" ], "properties": { "flow": { "type": "string", "maxLength": 2048 }, "event": { "type": "object", "additionalProperties": false, "required": [ "id", "provider", "event" ], "properties": { "id": { "type": "string", "format": "uuid" }, "provider": { "type": "string", "enum": [ "spotify", "apple_music" ] }, "event": { "type": "string", "enum": [ "connected", "playing", "paused", "stopped", "track_changed", "skip", "heartbeat", "disconnected" ] }, "trackId": { "type": [ "string", "null" ] }, "positionMs": { "type": "integer", "minimum": 0, "maximum": 86400000, "default": 0 }, "listenedMs": { "type": "integer", "minimum": 0, "maximum": 30000, "default": 0 } } } } } } } } } } }, "components": { "schemas": { "ListeningReceipt": { "type": "object", "properties": { "success": { "type": "boolean", "const": true }, "recorded": { "type": "boolean", "description": "False for an already-recorded retry." } }, "required": [ "success", "recorded" ] }, "PlayerError": { "type": "object", "properties": { "error": { "type": "string" }, "status": { "type": "string" } }, "required": [ "error" ] } } } } ``` # Exchange player Spotify authorization Source: https://recoupable.dev/docs/api-reference/complete/players/post-players-spotify-session Internal trusted Recoup player endpoint, not an external website integration. Requires a fresh Spotify listening session, PKCE code/verifier, API-configured OAuth app and required scopes. Captures Spotify-confirmed available profile/email for the registered artist/workspace. Returns provider credentials only to the trusted Recoup browser, plus player_session_id and fanCapture; no email/fan ID in the response. Tokens are not persisted in fan tables. A failed capture reports fanCapture=false while valid playback credentials remain usable. Authorization is not email marketing consent. ## POST /api/players/spotify/session Full OpenAPI specification: https://recoupable.dev/docs/spec/players.json ## Authentication This operation's specification permits a request without authentication. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Release Players", "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/players/spotify/session": { "post": { "summary": "Exchange player Spotify authorization", "description": "Internal trusted Recoup player endpoint, not an external website integration. Requires a fresh Spotify listening session, PKCE code/verifier, API-configured OAuth app and required scopes. Captures Spotify-confirmed available profile/email for the registered artist/workspace. Returns provider credentials only to the trusted Recoup browser, plus player_session_id and fanCapture; no email/fan ID in the response. Tokens are not persisted in fan tables. A failed capture reports fanCapture=false while valid playback credentials remain usable. Authorization is not email marketing consent.", "security": [], "parameters": [], "responses": { "200": { "description": "SpotifyPlayerSession fields returned by the release player service.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpotifyPlayerSession" } } } }, "400": { "description": "Invalid input", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "401": { "description": "Missing/invalid authentication or failed provider authorization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "403": { "description": "Workspace, origin, or session access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "404": { "description": "Player not available", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "429": { "description": "Request rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "503": { "description": "Feature/configuration temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } } }, "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": false, "required": [ "code", "verifier", "flow" ], "properties": { "code": { "type": "string", "minLength": 1, "maxLength": 2048 }, "verifier": { "type": "string", "minLength": 43, "maxLength": 128 }, "flow": { "type": "string", "minLength": 1, "maxLength": 2048 } } } } } } } } }, "components": { "schemas": { "SpotifyPlayerSession": { "type": "object", "properties": { "access_token": { "type": "string", "description": "Private provider access token, delivered only to the trusted Recoup browser. Never embed in an artist website." }, "refresh_token": { "type": "string", "description": "Optional private provider refresh token; same trusted-browser boundary." }, "expires_in": { "type": "number" }, "token_type": { "type": "string" }, "scope": { "type": "string" }, "player_session_id": { "type": "string", "format": "uuid" }, "fanCapture": { "type": "boolean", "description": "True only after the verified fan identity was persisted." } }, "required": [ "access_token", "expires_in", "scope", "player_session_id", "fanCapture" ] }, "PlayerError": { "type": "object", "properties": { "error": { "type": "string" }, "status": { "type": "string" } }, "required": [ "error" ] } } } } ``` # Acquire or resume a registered player session Source: https://recoupable.dev/docs/api-reference/complete/players/post-players-public-id-session Internal transport for the trusted Recoup browser player. Without provider, returns only public release settings. With provider, validates parent and creates or resumes a signed, provider-bound listening session. No owner, fan profile or email is exposed. The embedding website should use returned embed URLs rather than calling this API or handling credentials. ## POST /api/players/public/{id}/session Full OpenAPI specification: https://recoupable.dev/docs/spec/players.json ## Authentication This operation's specification permits a request without authentication. [Authentication guide](https://recoupable.dev/docs/authentication) ## Operation and referenced schemas ```json { "openapi": "3.1.0", "info": { "title": "Recoup API - Release Players", "version": "1.0.0" }, "servers": [ { "url": "https://api.recoupable.dev" } ], "paths": { "/api/players/public/{id}/session": { "post": { "summary": "Acquire or resume a registered player session", "description": "Internal transport for the trusted Recoup browser player. Without provider, returns only public release settings. With provider, validates parent and creates or resumes a signed, provider-bound listening session. No owner, fan profile or email is exposed. The embedding website should use returned embed URLs rather than calling this API or handling credentials.", "security": [], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "PublicPlayerConfig fields returned by the release player service.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicPlayerConfig" } } } }, "400": { "description": "Invalid input", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "401": { "description": "Missing/invalid authentication or failed provider authorization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "403": { "description": "Workspace, origin, or session access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "404": { "description": "Player not available", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "429": { "description": "Request rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } }, "503": { "description": "Feature/configuration temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlayerError" } } } } }, "operationId": "acquirePlayerSession", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "provider": { "type": "string", "enum": [ "spotify", "apple_music" ] }, "parent": { "type": "string", "minLength": 1 }, "flow": { "type": "string" }, "source": { "type": "string", "maxLength": 100 }, "medium": { "type": "string", "maxLength": 100 }, "campaign": { "type": "string", "maxLength": 100 }, "content": { "type": "string", "maxLength": 100 } }, "required": [ "provider", "parent" ], "additionalProperties": false } } } } } } }, "components": { "schemas": { "PublicPlayerConfig": { "type": "object", "properties": { "playerId": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "artwork": { "type": [ "string", "null" ], "format": "uri" }, "spotifyUrl": { "type": [ "string", "null" ], "format": "uri" }, "appleUrl": { "type": [ "string", "null" ], "format": "uri" }, "revision": { "type": "integer", "minimum": 1 }, "provider": { "type": "string", "enum": [ "spotify", "apple_music" ] }, "release": { "type": "string", "format": "uri" }, "sessionId": { "type": "string", "format": "uuid" }, "flow": { "type": "string", "description": "Short-lived signed player capability; only the trusted Recoup player should handle this." }, "spotify": { "type": "object", "properties": { "configured": { "type": "boolean" }, "clientId": { "type": [ "string", "null" ] }, "redirectUri": { "type": "string", "format": "uri" }, "scopes": { "type": "array", "items": { "type": "string" } } }, "required": [ "configured", "clientId", "redirectUri", "scopes" ] }, "freePlayback": { "type": "string", "enum": [ "spotify", "audio" ], "default": "spotify", "description": "Release-owner choice for verified Spotify Free accounts. spotify opens the configured Spotify release; audio plays a workspace-owned uploaded file. Premium continues using Spotify streaming." }, "audioUrl": { "type": [ "string", "null" ], "format": "uri", "description": "Included only in provider-session responses. Configured uploaded audio is provided for Spotify sessions in audio mode; otherwise null. Not included in chooser metadata." } }, "required": [ "playerId", "name", "spotifyUrl", "appleUrl", "revision", "freePlayback" ] }, "PlayerError": { "type": "object", "properties": { "error": { "type": "string" }, "status": { "type": "string" } }, "required": [ "error" ] } } } } ```