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.
Provenance & Partner Billing
ONCE classifies every MCP JSON-RPC request with a provenance used for both auditing and billing:
| Value | Detected via | Billing rule |
|---|---|---|
AIMD | Request Origin / Referer matches app.aimusicdistributor.com, or the JSON-RPC arguments include "provenance": "AIMD" (for server-to-server calls). | 2 credits per AI-detected song (1 base + 1 surcharge). All other songs remain 1 credit. |
MCP | Every other authenticated MCP client (default). | 1 credit per song. |
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
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",
"release_date": "2026-02-01",
"cover_art_file_url": "/api/files/cover-art/USER_ID/cover.jpg"
},
"tracks": [
{
"title": "My Release",
"primary_artist_name": "Artist Name",
"audio_file_url": "/api/files/audio/USER_ID/track.wav",
"explicit_flag": false,
"writers": [{ "name": "First Last" }]
}
],
"releaseId": "optional",
"conversationId": "optional"
}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.
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_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 an embedded Stripe Checkout Session for buying ONCE credits from an MCP client such as AIMD. The response includes the Stripe clientSecret and ONCE’s Stripe publishableKey, so the client can render embedded Checkout without separately configuring Stripe environment variables.
{
"credits": 5,
"successPath": "https://app.aimusicdistro.com/api/once/checkout-return",
"provenance": "AIMD"
}credits is required and must be an integer from 1 to 1000. successPath is optional; ONCE defaults to AIMD’s checkout return endpoint and automatically includes session_id={CHECKOUT_SESSION_ID} for Stripe’s hosted fallback. The checkout session metadata is written as user_id, credits, and source: "aimd" so the existing Stripe webhook can grant the purchased credits.
{
"clientSecret": "cs_test_...",
"sessionId": "cs_test_...",
"publishableKey": "pk_live_...",
"amount": 500,
"currency": "usd",
"credits": 5,
"expiresAt": "2026-05-19T15:30:00.000Z"
}get_profile and get_credits also have REST equivalents at GET /api/profile and GET /api/credits (cookie-authenticated) for the ONCE web app.