REST APIReleases

Releases

Once your assets are uploaded, you can save a draft and submit a release for distribution. All endpoints require authentication.

Read the rules first

Before building payloads, read GET /v1/metadata-rules (artist-name policy, title casing, writers, UPC, AI disclosure, cover songs) and GET /v1/release-schema (authoritative required fields and an example payload). Use GET /v1/distribution-stores to fetch supported DSP IDs before setting release.distribution_store_ids. The platform enforces these rules at submission, so reading them first saves you rejected requests.

curl https://once.app/v1/metadata-rules
curl -H "Authorization: Bearer $ONCE_ACCESS_TOKEN" https://once.app/v1/release-schema
curl https://once.app/v1/distribution-stores

Drafts

curl -X POST https://once.app/v1/drafts \
  -H "Authorization: Bearer $ONCE_ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{ "release": { ... }, "tracks": [ ... ], "mode": "delta" }'

POST /v1/drafts upserts a work-in-progress release snapshot. Body: { "release"?, "tracks"?, "track_patches"?, "upload_requests"?, "status"?, "mode"?: "delta" | "replace", "release_id"?, "conversation_id"? }. Use mode: "delta" to patch an existing draft or mode: "replace" to overwrite it.

To update an existing draft, pass its release_id (from GET /v1/releases) or the conversation_id of the chat it belongs to. When neither is provided, ONCE creates a new draft release from the payload and returns its id. The draft appears in the app immediately, and the response includes releaseCreated: true. Pass the returned releaseId as release_id on follow-up calls so edits land on the same draft instead of creating a new one.

{
  "releaseId": "6f0f0a4e-...",
  "releaseCreated": true,
  "snapshot": { "revision": 1, "snapshotId": "...", "status": "collecting" },
  "latest": { "payload": { ... }, "tracks": [ ... ] }
}

Submit a release

curl -X POST https://once.app/v1/releases \
  -H "Authorization: Bearer $ONCE_ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{ "release": { ... }, "tracks": [ ... ] }'

POST /v1/releases submits the release for distribution and returns 201. Build the body from GET /v1/release-schema, attaching the fileUrls returned by your uploads and cover-art calls. Submission debits credits from your account (1 credit per human song, 2 per AI-detected song). Releases are distributed to all supported stores unless release.distribution_store_ids selects specific DSPs.

New clients should provide DSP selection, record label, C/P copyright credits, track type, and Producer/Engineer role credits explicitly. Existing v1 clients that omit those expanded fields continue to work with ONCE’s server defaults.

{
  "release": {
    "title": "My Release",
    "primary_artist_name": "Artist Name",
    "genre": "Pop",
    "sub_genre": "Dance Pop",
    "release_date": "2026-02-01",
    "label": "Artist Name Records",
    "audio_language": "en",
    "metadata_language": "en",
    "distribution_store_ids": [1, 9, 13, 319],
    "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",
      "preview_start_seconds": 30,
      "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" }
      ]
    }
  ]
}

For DSP selection, pass a non-empty distribution_store_ids array using store IDs from GET /v1/distribution-stores, or pass null to explicitly choose all supported stores. Do not send an empty array.

spotify_artist_url and apple_music_artist_url map the release to one artist profile, meaning the main primary artist. For collaborations, send every primary artist in release.primary_artists, in credit order, each with their own profile links, so each artist maps to their existing store profile instead of being matched by name:

{
  "release": {
    "title": "Collab",
    "primary_artist_name": "FussyCraft",
    "primary_artists": [
      {
        "name": "FussyCraft",
        "spotify_artist_url": "https://open.spotify.com/artist/SPOTIFY_ARTIST_ID",
        "apple_music_artist_url": "https://music.apple.com/us/artist/fussycraft/1234567890"
      },
      {
        "name": "Michal - Mad Max",
        "spotify_artist_url": "https://open.spotify.com/artist/SPOTIFY_ARTIST_ID",
        "apple_music_artist_url": "https://music.apple.com/us/artist/michal-mad-max/987654321"
      }
    ]
  }
}
  • The first entry is the main primary artist; when primary_artist_name is also sent it must match that entry. Every following entry is credited as a Primary Artist contributor, in order.
  • Links must be artist profile URLs (https://open.spotify.com/artist/…, https://music.apple.com/<storefront>/artist/<slug>/<id>). Album, song, and playlist links are rejected with a 422 naming the field. Bare Spotify artist IDs and spotify:artist: URIs are accepted.
  • Omit any link you don’t have; soundcloud_artist_url and meta_artist_url work on the same entries. Tracks accept primary_artists too, for releases whose tracks are credited to different artists.
  • Positional arrays are also accepted: spotify_artist_urls and apple_music_artist_urls map entry N to primary artist N, with null holding a position. Sending more links than primary artists is rejected.

Nothing changes for single-artist releases: keep using the flat spotify_artist_url / apple_music_artist_url fields. Drafts (POST /v1/drafts) accept the same fields, and GET /v1/releases/:id returns the stored per-artist links as release.artist_profiles.

For remixes, do not send remix as track_type. Use title_version: "Remix" and include a Remixer contributor when applicable.

Partners can attach the X-Once-Provenance header (e.g. AIMD) so the release is attributed to their surface.

After submission, poll release status to follow delivery.

List releases

curl -H "Authorization: Bearer $ONCE_ACCESS_TOKEN" \
  'https://once.app/v1/releases?limit=100'

GET /v1/releases returns recent releases for the authenticated user (limit-only paging). GET /v1/releases/:id returns the merged metadata for a single release (its draft snapshot or latest merged data).