API Reference
HTTP endpoints for ONCE MCP. User-scoped endpoints usually require Authorization: Bearer <token>. Large-file upload chunk + complete requests can also use the scoped x-mcp-upload-token returned by prepare_local_file_upload.
Base URL: https://beta.once.app
Note: MCP submissions debit credits from the authenticated ONCE account.
Tool name resolution: calling an unknown tool name returns an error with did-you-mean suggestions plus a pointer to tools/list, and common aliases (for example release_status or check_release_status for get_release_status) resolve to the canonical tool automatically.
Provenance & Billing
ONCE classifies every MCP JSON-RPC request with a provenance used for auditing and analytics. Billing is the same across every surface: provenance does not change the rate.
| Value | Detected via |
|---|---|
AIMD | Request Origin / Referer matches app.aimusicdistributor.com, or the JSON-RPC arguments include "provenance": "AIMD" (for server-to-server calls). |
MCP | Every other authenticated MCP client (default). |
Billing rule (applies to every surface):
- 1 credit per human song
- 2 credits per AI song (AI-detected by the audio scanner, or generated through OMG)
The provenance is persisted on the release row (releases.mcp_provenance) and surfaced in the admin AI dashboard alongside the AI model classification.
Authentication
OAuth Metadata Discovery
Protected Resource Metadata for MCP OAuth discovery.
OAuth Authorization Server metadata (RFC 8414).
OpenID Connect metadata compatibility endpoint.
OAuth Endpoints
Authorization endpoint for MCP client browser sign-in.
Token endpoint for authorization code + PKCE exchange and refresh token grants.
Dynamic client registration endpoint for MCP client interoperability.
Most MCP clients use these automatically after a 401 Unauthorized challenge from /api/mcp.
First-Party Account Tools
The MCP server exposes two unauthenticated account tools for allowlisted first-party clients such as AIMD. They are still called through POST /api/mcp as JSON-RPC tools/call requests, require PKCE (code_challenge_method: "S256"), and return a short-lived, one-shot authorization_code that must be exchanged at /oauth/token with the matching code_verifier.
Random dynamic-client-registration clients cannot call these tools. ONCE checks the supplied client_id against a server-side allowlist and rate-limits by both IP and client_id.
Tool: create_account
Creates an ONCE account for AIMD and returns an OAuth authorization code.
{
"email": "artist@example.com",
"password": "correct-horse-battery-staple",
"first_name": "Ada",
"last_name": "Lovelace",
"client_id": "AIMD_DCR_CLIENT_ID",
"redirect_uri": "https://app.aimusicdistributor.com/oauth/callback",
"code_challenge": "BASE64URL_SHA256_VERIFIER",
"code_challenge_method": "S256",
"scope": "once:mcp",
"provenance": "AIMD"
}{
"userId": "uuid",
"email": "artist@example.com",
"authorization_code": "..."
}Typed error.data.code values: email_taken, weak_password, invalid_client, pkce_required.
Tool: password_authorize
Authorizes an existing ONCE account for AIMD and returns the same PKCE-bound authorization code shape.
{
"email": "artist@example.com",
"password": "correct-horse-battery-staple",
"client_id": "AIMD_DCR_CLIENT_ID",
"redirect_uri": "https://app.aimusicdistributor.com/oauth/callback",
"code_challenge": "BASE64URL_SHA256_VERIFIER",
"code_challenge_method": "S256",
"scope": "once:mcp",
"provenance": "AIMD"
}Typed error.data.code values: invalid_credentials, account_locked, invalid_client, pkce_required.
Uploads
Multipart Upload
Upload cover art or audio via multipart form-data.
Form Fields
| Field | Type | Description |
|---|---|---|
file | binary | The file to upload |
type | string | coverArt or audio |
Response
{
"success": true,
"fileUrl": "/api/files/cover-art/USER_ID/file.jpg",
"fileName": "file.jpg",
"userId": "..."
}Local Upload Session
For large local files in Claude Code or Cursor, call the prepare_local_file_upload MCP tool first. It returns:
session_idupload_tokenupload_header_name(x-mcp-upload-token)chunk_endpointcomplete_endpointrecommended_chunk_bytesmax_chunk_bytes
Upload Chunk
Upload a single chunk using the scoped token returned by prepare_local_file_upload.
Headers
x-mcp-upload-token: <upload_token>- For raw bytes:
x-session-id,x-chunk-index,x-upload-type
Multipart Fields
chunkchunkIndexsessionIdtype
JSON Request
{
"chunkBase64": "...",
"chunkIndex": 0,
"sessionId": "uuid-v4",
"type": "audio"
}Raw Bytes Body
Send the chunk as application/octet-stream with the headers listed above.
For large uploads, prefer raw bytes or multipart over JSON base64 to avoid extra payload overhead.
Complete Chunked Upload
Finalize a chunked upload session.
Request
{
"sessionId": "uuid-v4",
"fileName": "Track.wav",
"fileType": "audio/wav",
"type": "audio"
}Include x-mcp-upload-token: <upload_token> on the completion request.
Drafts
Save Draft
Persist a draft snapshot before submission.
Request
{
"releaseId": "optional",
"conversationId": "optional",
"mode": "delta",
"release": { ... },
"tracks": [ ... ],
"trackPatches": [ ... ],
"uploadRequests": [ ... ],
"status": "collecting"
}Releases
Get Distribution Stores
Tool: get_distribution_stores
Returns supported DSP/store IDs for release.distribution_store_ids. Send a
non-empty array to select DSPs, or null to distribute to all supported stores.
Set Distribution Stores
Tool: set_distribution_stores
Set or reset a release’s target store list. The change applies to the next submission or redistribution of the release.
| Argument | Type | Description |
|---|---|---|
releaseId | string | Required. ONCE release id. Must be owned by the user. |
storeIds | number[] or null | Revelator store ids from get_distribution_stores to target, or null to reset to the default set. |
Response
{
"ok": true,
"releaseId": "uuid",
"distributionStoreIds": [1, 9, 13],
"note": "Store targeting saved. It applies to the next submission or redistribution of this release."
}Passing an array with no valid store ids returns an error pointing back to
get_distribution_stores.
Submit Release
Submit a release for distribution. Rate limited; check retryAfterSeconds on 429.
Request
{
"release": {
"title": "My Release",
"primary_artist_name": "Artist Name",
"genre": "Pop",
"sub_genre": "Dance Pop",
"release_date": "2026-02-01",
"label": "Artist Name Records",
"distribution_store_ids": [1, 9, 13, 319],
"audio_language": "en",
"metadata_language": "en",
"pline_year": "2026",
"pline_owner": "Artist Name Records",
"cline_year": "2026",
"cline_owner": "Artist Name Records",
"cover_art_file_url": "/api/files/cover-art/USER_ID/cover.jpg",
"contributors": [
{ "name": "First Producer", "role": "Producer" },
{ "name": "First Engineer", "role": "Engineer" }
]
},
"tracks": [
{
"title": "My Release",
"primary_artist_name": "Artist Name",
"audio_file_url": "/api/files/audio/USER_ID/track.wav",
"explicit_flag": false,
"track_type": "original",
"language": "en",
"pline_year": "2026",
"pline_owner": "Artist Name Records",
"cline_year": "2026",
"cline_owner": "Artist Name Records",
"writers": [{ "name": "First Last" }],
"contributors": [
{ "name": "First Producer", "role": "Producer" },
{ "name": "First Engineer", "role": "Engineer" }
]
}
],
"releaseId": "optional",
"conversationId": "optional",
"tokenOffset": false
}The runtime MVP requirements remain backwards-compatible, but new clients should
provide DSP selection, record label, C/P copyright credits at release and track
level, track_type, and Producer/Engineer role credits. For remixes, use
title_version: "Remix" plus a Remixer contributor; do not send remix as
track_type.
Optional add-on
| Field | Type | Description |
|---|---|---|
tokenOffset | boolean | Default false. When true, charges a flat +1 credit ($1) that funds tokenoffset.com to offset the AI environmental cost of the release. Recorded in release_token_offsets and as a separate credit_transactions row with ref=token_offset:<release_id>. Available on every MCP surface: agents should offer the choice to the user before calling submit_release. |
List Releases
Get recent releases for the authenticated user.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
limit | number | Max results (default 100) |
Get Release Metadata
Get merged metadata for a release.
Get Release Status
Get store delivery and aggregate status.
Get Job Status
Get processing job status and errors.
AI Cover Art Generation
The generate_cover_art MCP tool produces (or edits) an album cover from a text prompt, runs the result through the same square-crop pipeline as user uploads, and stores it in the caller’s cover-art bucket. The returned fileUrl is a drop-in replacement for release.cover_art_file_url in submit_release.
Tool: generate_cover_art
| Argument | Type | Description |
|---|---|---|
prompt | string | Required. 1-1500 character description of the desired artwork. When editing, describe what should change. |
baseFileUrl | string | Optional. A fileUrl from a previous generate_cover_art or upload_file call. Must point to the caller’s cover-art bucket. The new image becomes an edit of this base image. |
baseImageBase64 | string | Optional. Base64-encoded base image (alternative to baseFileUrl). Accepts a data: prefix or raw base64. |
baseImageMimeType | string | Optional MIME override for baseImageBase64 (e.g. image/png, image/jpeg). |
releaseId | string | Optional release ID (must be owned by the user) for cost attribution. |
conversationId | string | Optional conversation ID for cost attribution. |
returnBase64 | boolean | If true, also return the raw image base64 alongside the fileUrl. |
fileName | string | Optional filename hint (defaults to mcp-cover-<timestamp>.png). |
Response
{
"fileUrl": "/api/files/cover-art/USER_ID/<uuid>.png",
"bucket": "cover-art",
"key": "USER_ID/<uuid>.png",
"fileName": "<uuid>.png",
"prompt": "Lo-fi vinyl in a sunset cafe, warm pastels, 70s poster art",
"model": "gemini-3.1-flash-image-preview",
"modelVariant": "nano_banana_2",
"imageSize": "1K",
"aspectRatio": "1:1",
"edited": false,
"rateLimit": { "remaining": 4 }
}edited is true whenever the call passed a base image so Gemini was asked to refine it.
Iterative Editing
To iterate on a previous generation (the same flow as the in-app cover art generator), pass the prior fileUrl and a delta prompt:
{
"jsonrpc": "2.0",
"id": 12,
"method": "tools/call",
"params": {
"name": "generate_cover_art",
"arguments": {
"prompt": "Make the sky more dramatic and add neon signage in the window",
"baseFileUrl": "/api/files/cover-art/USER_ID/<previous-uuid>.png"
}
}
}baseFileUrl must belong to the authenticated user and live in the cover-art bucket. Use baseImageBase64 instead if you already have the bytes locally (e.g. you set returnBase64: true on a prior call).
Model Tier
generate_cover_art always uses the non-patron Nano Banana 2 model (gemini-3.1-flash-image-preview) at 1K. Patron-only model variants are not exposed through the MCP, patrons that want higher fidelity should use the in-app cover art generator.
Rate Limits
Shared with the in-app generator under the cover_art_generate budget (default: 5 requests per 2 minutes per user). On rejection, the JSON-RPC response carries error.code = 429 and error.data.retryAfterSeconds.
AI Detection
ONCE runs every audio upload through the Vobile/Pex AI Song Detector. The detect_audio_ai MCP tool exposes the cached result so agents can show the AI flag and predicted-model classification before submission.
Tool: detect_audio_ai
| Argument | Type | Description |
|---|---|---|
fileUrl | string | Required. fileUrl returned by an MCP upload tool (must point to the audio bucket). |
fileName | string | Optional original filename for diagnostics. |
releaseId | string | Optional release ID to associate the detection event with. |
conversationId | string | Optional conversation ID for snapshot writeback. |
trackIndex | number | Optional 0-based track index used to update the matching draft track. |
Response
{
"status": "ok",
"containsAi": true,
"aiScore": 0.93,
"predictedModel": "Suno",
"predictedModelScore": 0.81,
"provider": "pex",
"model": "ai-song-detector",
"message": null,
"cached": false
}status is ok whenever the detector returned a result (regardless of containsAi). Other values include skipped, download_failed, and too_large. Detection results are cached per file, so calling this tool repeatedly for the same fileUrl returns the cached result with "cached": true.
Performance Analytics
Two read-only MCP tools report streaming performance for the authenticated user’s releases. Figures come from ONCE’s cached Revelator analytics (refreshed periodically) with a live fallback, so they match the in-app performance dashboard. YouTube Music view deltas are folded in as a synthetic store with distributorId: -1300.
Both tools default to the trailing 30 days. Pass fromDate/toDate (YYYY-MM-DD) for any window up to 2 years. KPIs share this shape:
| KPI | Description |
|---|---|
totalStreams | Total streams in the window. |
avgDailyStreams | totalStreams divided by the number of days in the window. |
periodChangePct | Percent change vs the immediately-preceding window of equal length. null when there is no prior data. |
topStore | { name, share } for the leading store (share is 0–1). null when filtered to a single store. |
Newly distributed releases can return zeros until DSPs report and the cache backfills.
Tool: get_performance_summary — catalog overview
| Argument | Type | Description |
|---|---|---|
fromDate | string | Optional window start (YYYY-MM-DD). Defaults to 30 days before toDate. |
toDate | string | Optional window end (YYYY-MM-DD). Defaults to today. |
topReleasesLimit | number | Optional. How many top releases to return (1–50, default 10). |
topStoresLimit | number | Optional. How many top stores to return (1–50, default 10). |
{
"fromDate": "2026-05-20",
"toDate": "2026-06-19",
"releases": { "total": 8, "withRevelatorId": 6 },
"metrics": [
{ "eventDate": "2026-05-20", "streamsCount": 184 },
{ "eventDate": "2026-05-21", "streamsCount": 203 }
],
"kpis": {
"totalStreams": 5821,
"avgDailyStreams": 187.8,
"periodChangePct": 12.4,
"topStore": { "name": "Spotify", "share": 0.62 }
},
"topStores": [
{ "id": 9, "name": "Spotify", "total": 3609 },
{ "id": 1, "name": "Apple Music", "total": 1402 },
{ "id": -1300, "name": "YouTube Music", "total": 810 }
],
"topReleases": [
{
"releaseId": "uuid",
"title": "My Release",
"primaryArtistName": "Artist Name",
"coverArtFileUrl": "/api/files/cover-art/USER_ID/cover.jpg",
"streamsCount": 4123
}
]
}Tool: get_release_performance — single release (and tracks)
| Argument | Type | Description |
|---|---|---|
releaseId | string | Required. ONCE release id (from list_releases). Must be owned by the user. |
fromDate | string | Optional window start (YYYY-MM-DD). Defaults to 30 days before toDate. |
toDate | string | Optional window end (YYYY-MM-DD). Defaults to today. |
distributorId | number | Optional store filter. Use an id from distributors/topStores (-1300 = YouTube Music). Omit for all stores. |
includeTracks | boolean | When true, include a per-track streams breakdown for the window. |
{
"releaseId": "uuid",
"fromDate": "2026-05-20",
"toDate": "2026-06-19",
"distributorId": null,
"metrics": [
{ "eventDate": "2026-05-20", "streamsCount": 42 }
],
"kpis": {
"totalStreams": 1280,
"avgDailyStreams": 41.3,
"periodChangePct": -3.1,
"topStore": { "name": "Spotify", "share": 0.71 }
},
"distributors": [
{ "id": 9, "name": "Spotify", "total": 909 },
{ "id": 1, "name": "Apple Music", "total": 371 }
],
"tracks": [
{ "trackId": 12345, "trackName": "Lead Single", "artistName": "Artist Name", "streamsCount": 880 }
],
"source": "cache"
}tracks is only populated when includeTracks: true. source is "cache" or "live" so you can tell whether the figures came from the cache or a fresh Revelator pull. Performance is only available for distributed releases the user owns; requests for other releases return 403 Forbidden.
Profile & Credits
These tools mirror the REST endpoints used by the ONCE app and are convenient when the agent needs to greet the user, pre-fill artist metadata, or check that the user has enough credits before submitting.
Tool: get_profile (no arguments)
{
"profile": {
"id": "...",
"email": "artist@example.com",
"first_name": "Ada",
"last_name": "Lovelace",
"created_at": "2026-02-01T12:00:00.000Z",
"avatar_url": "https://...signed-avatar-url...",
"is_patron": false,
"is_admin": false
}
}Tool: get_pricing (no arguments)
Static price list — call it to pick the cheapest way to cover a submission. Returns the per-credit price, per-song cost (1 credit per human song, 2 per AI song), the Token Offset add-on, every discounted bulk bundle with its packageId, and how auto-reload works. Mirrors the public GET /v1/pricing.
{
"currency": "usd",
"perCredit": { "usd": 1, "cents": 100 },
"perSong": { "human": 1, "ai": 2, "aiSurcharge": 1 },
"tokenOffset": { "credits": 1, "costUsd": 1, "optional": true },
"bundles": [
{ "packageId": "credits_20", "credits": 20, "priceUsd": 19, "perCreditUsd": 0.95, "discountPercent": 5, "savingsUsd": 1, "tag": "EP & album" },
{ "packageId": "credits_100", "credits": 100, "priceUsd": 88, "perCreditUsd": 0.88, "discountPercent": 12, "savingsUsd": 12, "tag": "Most popular" }
],
"autoReload": { "available": true, "requiresSavedCard": true, "mcp": { "read": "get_autoreload", "configure": "set_autoreload" } },
"creditsNeverExpire": true
}Tool: get_credits
| Argument | Type | Description |
|---|---|---|
transactionLimit | number | Optional. How many recent transactions to include (0–100, default 10). |
{
"balance": 12,
"updatedAt": "2026-05-01T08:00:00.000Z",
"transactions": [
{ "id": "...", "type": "debit", "amount": 1, "balance_after": 12, "created_at": "...", "ref": "release:..." }
],
"aggregates": {
"usedTotal": 4,
"purchasedTotal": 16
}
}Tool: create_credit_checkout_session
Creates a Stripe Checkout Session for buying ONCE credits. Buy a discounted bulk bundle by passing packageId (see get_pricing), or a custom amount at the flat $1/credit rate by passing credits. Two modes:
uiMode: "hosted"(recommended for agents): returns a Stripe-hosted Checkouturl. Any payment-capable agent or agentic-commerce flow can complete it directly; otherwise hand the link to the user as the single manual step. Credits are granted automatically by webhook when payment succeeds — confirm withget_credits.uiMode: "embedded"(default): returns a StripeclientSecretplus ONCE’spublishableKeyfor rendering embedded Checkout in a web UI (this is what AIMD uses).
| Argument | Type | Description |
|---|---|---|
credits | integer | 1–1000 credits to purchase at $1 each. Omit when buying a bundle. |
packageId | string | Bulk bundle id from get_pricing (e.g. "credits_500"). Wins over credits, charged at the discounted price. |
savePaymentMethod | boolean | Optional. Save the card for reuse so you can then enable auto-reload via set_autoreload. Default false. |
uiMode | string | Optional. "hosted" or "embedded" (default). |
successPath | string | Optional post-payment redirect (absolute https URL or path). Hosted mode defaults to the ONCE dashboard; embedded mode defaults to AIMD’s checkout return endpoint. session_id={CHECKOUT_SESSION_ID} is appended automatically. |
provenance | string | Optional. AIMD passes "AIMD". |
Hosted request and response:
{
"credits": 5,
"uiMode": "hosted"
}{
"uiMode": "hosted",
"url": "https://checkout.stripe.com/c/pay/cs_live_...",
"sessionId": "cs_live_...",
"publishableKey": "pk_live_...",
"amount": 500,
"currency": "usd",
"credits": 5,
"expiresAt": "2026-05-19T15:30:00.000Z"
}Embedded responses keep the previous shape (clientSecret instead of url, plus uiMode: "embedded"). When a bundle is purchased the response also includes its packageId. The checkout session metadata is written as user_id and credits (with source: "mcp" for hosted or "aimd" for embedded) so the existing Stripe webhook grants the purchased credits automatically.
get_profile and get_credits also have REST equivalents at GET /api/profile and GET /api/credits (cookie-authenticated) for the ONCE web app.
Auto-reload
Auto-reload tops the balance back up off-session by charging a saved card whenever the balance reaches the threshold (0 by default), so a release never stalls mid-submission. Read it with get_autoreload; configure it with set_autoreload. REST equivalents: GET /v1/me/autoreload and POST /v1/me/autoreload.
Tool: get_autoreload (no arguments)
{
"enabled": true,
"configured": true,
"thresholdCredits": 0,
"packageId": "credits_100",
"reloadCredits": 100,
"reloadAmountCents": 5000,
"card": { "brand": "visa", "last4": "4242", "expMonth": 12, "expYear": 2030 },
"lastStatus": "succeeded",
"lastTriggeredAt": "2026-05-19T15:30:00.000Z"
}Tool: set_autoreload
| Argument | Type | Description |
|---|---|---|
enabled | boolean | Required. Turn auto-reload on or off. |
packageId | string | Bulk bundle to reload (see get_pricing). Wins over quantity. |
quantity | integer | Custom credits to reload at $1/credit when no bundle is chosen. |
paymentMethodId | string | Saved card to charge. Omit to reuse the most recent saved card. |
thresholdCredits | integer | Reload when the balance falls to/below this (default 0). |
Enabling needs a saved card. The simplest agent flow is to call create_credit_checkout_session with savePaymentMethod: true, then:
{
"enabled": true,
"packageId": "credits_100"
}The most recent saved card is selected automatically. Disable with { "enabled": false }. The response is the same shape as get_autoreload.