{"openapi":"3.1.0","info":{"title":"ONCE REST API","version":"1.0.0","description":"Human-friendly HTTP API for ONCE music distribution. Same capabilities, authentication, billing, and validation as the ONCE MCP server."},"servers":[{"url":"https://once.app/v1"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT or once_pat_*"}},"schemas":{"Error":{"type":"object","required":["type","code","message"],"properties":{"type":{"type":"string"},"code":{"type":"string"},"message":{"type":"string"},"retryAfterSeconds":{"type":"number"}}},"ContributorInput":{"type":"object","properties":{"name":{"type":"string"},"role":{"type":"string","examples":["Producer","Engineer","Remixer"]},"roleId":{"type":"number"},"contributorRoleGroupId":{"type":"number"}}},"ArtistLocalizationInput":{"type":"object","properties":{"name":{"type":"string"},"language":{"type":"string","description":"ISO 639-1 code such as ko, zh, ja; resolved to the Revelator language ID at submission. Preferred over languageId."},"languageId":{"type":"number","description":"Revelator-internal numeric language ID; provide language instead when only the ISO code is known."},"phoneticName":{"type":"string"}}},"WriterInput":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Full legal name"},"society":{"type":"string"},"ipi":{"type":"string"},"share_percent":{"type":"number"}}},"PrimaryArtistInput":{"type":"object","required":["name"],"description":"One primary artist plus that artist's own store profile links. Used to pin each artist of a collaboration to their existing artist page.","properties":{"name":{"type":"string"},"spotify_artist_url":{"type":"string","description":"Spotify artist profile URL (https://open.spotify.com/artist/…). Album/song links are rejected."},"apple_music_artist_url":{"type":"string","description":"Apple Music artist profile URL (https://music.apple.com/<storefront>/artist/<slug>/<id>). Album/song links are rejected."},"soundcloud_artist_url":{"type":"string"},"meta_artist_url":{"type":"string"}}},"ReleaseInput":{"type":"object","required":["title","primary_artist_name","genre","release_date","cover_art_file_url"],"properties":{"title":{"type":"string"},"title_version":{"type":"string","description":"Version qualifier (e.g. Remix, Acoustic Cover). Never the bare word \"Cover\" — mark covers with track_type instead."},"primary_artist_name":{"type":"string"},"primary_artists":{"type":"array","items":{"$ref":"#/components/schemas/PrimaryArtistInput"},"description":"Every primary artist in credit order. The first entry is the main primary artist and must match primary_artist_name when both are sent; the rest are added as \"Primary Artist\" contributors. Each entry's profile links map that artist to their existing store profile."},"spotify_artist_urls":{"type":"array","items":{"type":["string","null"]},"description":"Positional alternative to primary_artists: entry N is the Spotify artist profile link for primary artist N. Use null to hold a position. Cannot be longer than the primary artist list."},"apple_music_artist_urls":{"type":"array","items":{"type":["string","null"]},"description":"Positional Apple Music artist profile links, matched to the primary artists by index."},"spotify_artist_url":{"type":"string","description":"Spotify artist profile URL for the main primary artist."},"apple_music_artist_url":{"type":"string","description":"Apple Music artist profile URL for the main primary artist."},"genre":{"type":"string"},"sub_genre":{"type":"string","description":"Optional second genre"},"second_genre":{"type":"string","description":"Accepted alias for sub_genre"},"release_date":{"type":"string","format":"date"},"label":{"type":"string","description":"Record label name"},"audio_language":{"type":"string","examples":["en"]},"metadata_language":{"type":"string","examples":["en"]},"metadata_language_country":{"type":"string"},"distribution_store_ids":{"oneOf":[{"type":"array","items":{"type":"integer"},"minItems":1},{"type":"null"}],"description":"New clients should send a non-empty array of supported DSP store IDs, or null for all supported stores. Omitted legacy payloads default to all supported stores."},"pline_year":{"type":"string"},"pline_owner":{"type":"string"},"cline_year":{"type":"string"},"cline_owner":{"type":"string"},"cover_art_file_url":{"type":"string"},"contributors":{"type":"array","items":{"$ref":"#/components/schemas/ContributorInput"}},"artist_locals":{"type":"array","items":{"$ref":"#/components/schemas/ArtistLocalizationInput"}}}},"TrackInput":{"type":"object","required":["title","audio_file_url","explicit_flag","writers"],"properties":{"title":{"type":"string"},"title_version":{"type":"string","description":"Use Remix here for remixes; track_type does not accept remix. Never the bare word \"Cover\" — mark covers with track_type: \"cover\"; a version naming the kind of cover (e.g. \"Acoustic Cover\") is allowed."},"primary_artist_name":{"type":"string"},"primary_artists":{"type":"array","items":{"$ref":"#/components/schemas/PrimaryArtistInput"},"description":"Primary artists for this track, in credit order, when they differ from the release. Same shape and behaviour as release.primary_artists."},"audio_file_url":{"type":"string"},"explicit_flag":{"type":"boolean"},"track_type":{"type":"string","enum":["original","cover","public_domain"]},"isrc":{"type":"string","description":"Optional valid ISRC"},"genre":{"type":"string"},"language":{"type":"string","examples":["en"]},"lyrics":{"type":"string","description":"Plain-text lyrics"},"preview_start_seconds":{"type":"integer","minimum":0,"description":"Preview/clip start time in seconds. Where the clip starts when a listener previews the track. Defaults to 15 when omitted."},"pline_year":{"type":"string"},"pline_owner":{"type":"string"},"cline_year":{"type":"string"},"cline_owner":{"type":"string"},"writers":{"type":"array","items":{"$ref":"#/components/schemas/WriterInput"}},"contributors":{"type":"array","items":{"$ref":"#/components/schemas/ContributorInput"}},"artist_locals":{"type":"array","items":{"$ref":"#/components/schemas/ArtistLocalizationInput"}}}},"SubmitReleaseRequest":{"type":"object","required":["release","tracks"],"properties":{"release":{"$ref":"#/components/schemas/ReleaseInput"},"tracks":{"type":"array","items":{"$ref":"#/components/schemas/TrackInput"}},"release_id":{"type":"string"},"conversation_id":{"type":"string"},"token_offset":{"type":"boolean"},"distributors":{"type":"object"},"skip_adapters":{"type":"boolean"}}},"DraftRequest":{"type":"object","properties":{"release":{"$ref":"#/components/schemas/ReleaseInput"},"tracks":{"type":"array","items":{"$ref":"#/components/schemas/TrackInput"}},"track_patches":{"type":"array","items":{"type":"object"}},"upload_requests":{"type":"array","items":{"type":"object"}},"status":{"type":"string"},"mode":{"type":"string","enum":["delta","replace"]},"release_id":{"type":"string","description":"Existing draft release to update. Omit together with conversation_id to create a new draft release."},"conversation_id":{"type":"string","description":"Existing chat conversation whose draft should be updated."}}},"FraudScoreRequest":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","description":"Email address to check."}}},"FraudFlag":{"type":"object","required":["code","label","category","severity","triggered","points"],"properties":{"code":{"type":"string","description":"Stable public flag code."},"label":{"type":"string"},"category":{"type":"string","enum":["rights","account_integrity","content","distribution","streaming","behavior","abuse"]},"severity":{"type":"string","enum":["low","medium","high","critical"]},"triggered":{"type":"boolean"},"points":{"type":"integer","description":"Points this flag contributed (0 when not triggered)."}}},"FraudScoreResponse":{"type":"object","required":["email","score","maxScore","severity","flags"],"properties":{"email":{"type":"string","format":"email"},"score":{"type":"integer","description":"Overall weighted risk score, 0..maxScore."},"maxScore":{"type":"integer","description":"Scoring ceiling."},"severity":{"type":"string","enum":["none","low","medium","high","critical"]},"computedAt":{"type":["string","null"],"format":"date-time","description":"Last time this account was scored, or null if never scored."},"flags":{"type":"array","items":{"$ref":"#/components/schemas/FraudFlag"}}}},"DistributionStoresResponse":{"type":"object","properties":{"stores":{"type":"array","items":{"type":"object","properties":{"storeId":{"type":"integer"},"name":{"type":"string"},"featured":{"type":"boolean"},"enabledByDefault":{"type":"boolean"},"aliases":{"type":"array","items":{"type":"string"}}}}},"defaultStoreIds":{"type":"array","items":{"type":"integer"}},"selectionSemantics":{"type":"object"}}}}},"security":[{"bearerAuth":[]}],"paths":{"/":{"get":{"summary":"API index","security":[],"responses":{"200":{"description":"API index payload"}}}},"/agent-auth/verified-email/challenge":{"post":{"summary":"Request a verified-email agent credential code","description":"Auth.md verified_email flow, step 1. Mails a one-time code to the ONCE account holder. The response is identical whether or not the address has an account. Rate limited per IP and per email.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","description":"Email of the ONCE account to act for."},"client_name":{"type":"string","description":"Agent name shown in the email and used as the issued token name."},"scopes":{"type":"array","items":{"type":"string"},"description":"Requested scopes. Defaults to once:mcp."}}}}}},"responses":{"200":{"description":"Generic acknowledgement with claim_uri and expires_in"},"429":{"description":"Rate limited"}}}},"/agent-auth/verified-email/claim":{"post":{"summary":"Exchange a verified-email code for a credential","description":"Auth.md verified_email flow, step 2. Returns a scoped bearer token the account owner can revoke at /settings/developer. Codes are single-use, expire in 15 minutes, and allow 5 attempts.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","code"],"properties":{"email":{"type":"string"},"code":{"type":"string","description":"The 8-digit code from the email."}}}}}},"responses":{"201":{"description":"access_token, token_type, scope, expires_at"},"400":{"description":"Invalid or expired code, or too many attempts"}}}},"/status":{"get":{"summary":"Service status","description":"Unauthenticated liveness probe. Referenced as the `status` link relation from /.well-known/api-catalog.","security":[],"responses":{"200":{"description":"Service is serving requests"}}}},"/metadata-rules":{"get":{"summary":"Metadata policy hitlist","security":[],"responses":{"200":{"description":"Markdown or JSON wrapper"}}}},"/distribution-stores":{"get":{"summary":"Supported DSP/store IDs for distribution_store_ids","security":[],"responses":{"200":{"description":"Supported DSP store catalog","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DistributionStoresResponse"}}}}}}},"/pricing":{"get":{"summary":"Credit pricing: per-credit, per-song (human vs AI), bundles, and auto-reload","security":[],"responses":{"200":{"description":"Pricing info including discounted bulk bundles"}}}},"/release-schema":{"get":{"summary":"Required release fields and example payload","responses":{"200":{"description":"Release schema JSON"}}}},"/me":{"get":{"summary":"Authenticated user profile","responses":{"200":{"description":"Profile payload"}}}},"/me/credits":{"get":{"summary":"Credit balance and recent transactions","parameters":[{"name":"transactions","in":"query","schema":{"type":"integer","minimum":0,"maximum":100}}],"responses":{"200":{"description":"Credits payload"}}}},"/files":{"post":{"summary":"Upload a small file as JSON base64","responses":{"200":{"description":"Uploaded file metadata"}}}},"/files/from-url":{"post":{"summary":"Import a file from a public URL","responses":{"200":{"description":"Uploaded file metadata"}}}},"/uploads":{"post":{"summary":"Create a chunked upload session","responses":{"200":{"description":"Upload session metadata"}}}},"/audio/ai-detection":{"post":{"summary":"AI Song Detector result for an uploaded file","responses":{"200":{"description":"AI detection result"}}}},"/audio/acr-check":{"post":{"summary":"Registry-match (Pex Search) check","responses":{"200":{"description":"ACR check result"}}}},"/cover-art":{"post":{"summary":"Generate or iteratively edit AI album art","responses":{"200":{"description":"Generated cover art metadata"}}}},"/drafts":{"post":{"summary":"Upsert a work-in-progress release snapshot","description":"Pass release_id (or conversation_id) to update an existing draft. When neither is provided, a new draft release is created from the payload and its releaseId is returned with releaseCreated: true — reuse it on follow-up calls. Drafts do not enforce submission validation, but the response reports submittable (boolean) and submissionWarnings (array of per-field messages) so an unsubmittable draft surfaces before POST /releases.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DraftRequest"}}}},"responses":{"200":{"description":"Draft snapshot result"}}}},"/releases":{"get":{"summary":"List recent releases","responses":{"200":{"description":"Release list"}}},"post":{"summary":"Submit a release for distribution","description":"Existing v1 clients may omit expanded metadata and receive server defaults. New clients should provide distribution_store_ids, label, copyright credits, track_type, and role credits explicitly. A 422 validation_failed response carries a details array naming each offending field path, e.g. tracks[0].writers[0].name.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitReleaseRequest"}}}},"responses":{"201":{"description":"Submitted release"}}}},"/releases/{id}":{"get":{"summary":"Merged metadata for a release","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Release metadata"}}}},"/releases/{id}/status":{"get":{"summary":"Store delivery and aggregate status","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Release status"}}}},"/releases/{id}/job":{"get":{"summary":"Background processing job state","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Release job"}}}},"/releases/{id}/distribute":{"post":{"summary":"Deliver an already-submitted release to additional stores","description":"Body: { store_ids: number[] } with Revelator store ids from GET /distribution-stores. Queues delivery of a live release to the extra stores without re-uploading assets. The response lists what was actually queued; requested stores that could not be delivered (e.g. Content ID without partner approval, UGC stores without a resolvable policy) come back in skippedStoreIds with a reason.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Queued store additions"},"403":{"description":"Store not available for this account or region"},"409":{"description":"Release has not been submitted/distributed yet"}}}},"/releases/{id}/takedown":{"post":{"summary":"Take a release down from specific stores or all stores","description":"Body: { store_ids: number[] } for specific stores, or { store_ids: \"all\" } (or an empty body) to request removal from every store ONCE distributes to. Takedowns propagate to stores over days and cannot be un-requested.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Takedown requested"},"409":{"description":"Release has not been submitted/distributed yet"}}}},"/releases/{id}/stores":{"put":{"summary":"Set the target store list for the next submission or redistribution","description":"Body: { store_ids: number[] | null }. Pass null to reset to the default set. This only stores targeting for the next submission/redistribution; use POST /releases/{id}/distribute to deliver a live release to more stores now.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Stored targeting"}}}},"/releases/{id}/ugc-policies":{"get":{"summary":"UGC monetization (match) policy selection for a release","description":"Current per-platform policy (with ONCE defaults filled in) plus the selectable catalog for YouTube Content ID (307), Facebook Rights Manager (310), and TikTok (319).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Current selection and available policies"}}},"put":{"summary":"Select UGC monetization (match) policies for a release","description":"Body: { policies: { \"<storeId|alias>\": \"<policyCode>\" } }, e.g. { \"policies\": { \"307\": \"track\", \"tiktok\": \"library_only\" } }. Codes per store — 307/youtube_content_id: monetize | track; 310/facebook_rights_manager: claim_ad_earnings | block | monitor; 319/tiktok: monetize | library_only | block. Pass null for a store to reset it to the ONCE default. The selection applies the next time the release is delivered to the affected stores (submission, redistribution, or POST /releases/{id}/distribute).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Stored selection"}}}},"/me/autoreload":{"get":{"summary":"Current auto-reload configuration","responses":{"200":{"description":"Auto-reload settings"}}},"post":{"summary":"Enable, disable, or reconfigure auto-reload","description":"Body: { enabled, package_id | quantity, payment_method_id?, threshold_credits? }. When enabling, a saved card is required; omit payment_method_id to reuse the most recent saved card. threshold_credits sets the balance level that triggers a reload (default 0 = only when fully empty; e.g. 10 tops up before running out).","responses":{"200":{"description":"Updated auto-reload settings"}}}},"/credits/checkout":{"post":{"summary":"Create a Stripe checkout session to buy credits","description":"Body: { credits } for a custom $1/credit amount, or { package_id } to buy a discounted bulk bundle (see GET /pricing). Optional { save_payment_method, ui_mode, success_path }.","responses":{"200":{"description":"Checkout session"}}}},"/webhooks":{"get":{"summary":"List registered release webhook endpoints","responses":{"200":{"description":"Webhook endpoint list (signing secrets are never returned)"}}},"post":{"summary":"Register a webhook endpoint for release events","description":"Body: { url, description?, event_types? }. Supported events: release.status_changed (every aggregate status transition), release.approved (fires once when the release passes distributor approval and goes live; the payload carries the UPC and every track ISRC), release.stores_added (an already-distributed release was queued for additional stores) and release.takedown_completed (stores confirmed a full or partial takedown). The last two have no aggregate status transition to ride on, so they only reach you as their own events. Every payload also carries artistProfiles with the Apple Music artist id and Spotify artist URI pinned to the release. New endpoints subscribe to all events unless event_types narrows the list. Endpoints must use https; up to 5 per account. The response includes the signing secret exactly once, so store it immediately.","responses":{"201":{"description":"Created endpoint with one-time signing secret"}}}},"/webhooks/{id}":{"patch":{"summary":"Enable/disable a webhook endpoint or change its event subscriptions","description":"Body: { active?: boolean, event_types?: string[] }. Re-enabling resets the failure count; event_types lets an endpoint opt into events added after it was created (an endpoint keeps the list stored at creation, so older endpoints must PATCH to receive release.stores_added and release.takedown_completed).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated endpoint"}}},"delete":{"summary":"Delete a webhook endpoint","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deletion confirmation"}}}},"/fraud/score":{"post":{"summary":"ONCE Fraud API — risk score and flags for an email","description":"Returns the overall weighted fraud score, severity, and the individual signal flags for an email. Requires an API key (Personal Access Token) with the once:fraud scope, created in the ONCE dev center. Never returns IPs, other-account data, or any internal detection details — only scores and flags. Free during a limited-time beta.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FraudScoreRequest"}}}},"responses":{"200":{"description":"Fraud score and flags","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FraudScoreResponse"}}}},"403":{"description":"API key missing the once:fraud scope"},"404":{"description":"No ONCE account found for the email"},"429":{"description":"Rate limit or daily free-tier quota exceeded"}}}},"/auth/token":{"post":{"summary":"OAuth 2.1 token endpoint","security":[],"responses":{"200":{"description":"Token response"}}}},"/accounts":{"post":{"summary":"Partner account provisioning","security":[],"responses":{"201":{"description":"Created account authorization code"}}}},"/auth/authorize":{"post":{"summary":"Partner password authorization","security":[],"responses":{"200":{"description":"Authorization code"}}}}}}