Skip to content

Guides

Deliver a release

From a draft to the stores: files, checks, review, delivery and the smart link, with the webhook for each step.

On this page

This guide follows one release from your system to the stores, and names the webhook that tells you about each step. The Quickstart tries the first steps in the sandbox; this is the whole road, as it runs with a live key.

The road at a glance#

StatusWhat is happeningEvents
DRAFTYou build the release: details, tracks, files
IN_REVIEWStillmore Group's review has itrelease.submitted
CHANGES_REQUESTEDReview sent it back with a noterelease.changes_requested
APPROVEDReview passed itrelease.approved
SENT_TO_STORESOn its way to the storesrelease.sent, release.codes_assigned
LIVEIn the storesrelease.live, release.store_links_updated
REFUSED_BY_STORESThe stores refused itrelease.provider_rejected
REJECTEDReview won't send itrelease.rejected

A release made by an artist account goes to its label first (LABEL_REVIEW); the label's approval sends it on to review.

1. Find or create the artists#

Look the artist up by your own id before creating one, so a retry or a second run never makes a duplicate. Store profiles put the release on the artist's existing page in each store.

import { StillmoreApi } from "./stillmore-api";

const api = new StillmoreApi({ apiKey: process.env.STILLMORE_API_KEY! });

const [found] = await api.get<Array<{ id: string }>>("/artists", { externalId: "artist-118" });
const artist = found ?? (await api.post<{ id: string }>("/artists", {
  name: "June Hale",
  externalId: "artist-118",
  profiles: { spotify: "https://open.spotify.com/artist/<id>" }
}));

2. Create the draft#

One call makes the release and its tracks. Give it everything you know now; anything left out can be added with PATCH while it is a draft. The field list says what each field takes.

const release = await api.post<{ id: string; tracks: Array<{ id: string }> }>("/releases", {
  title: "Night Signals",
  type: "SINGLE",
  primaryArtistIds: [artist.id],
  primaryGenre: "Pop",
  language: "en",
  releaseDate: "2027-01-15",
  copyright: { cLine: { year: 2026, holder: "North Records" }, pLine: { year: 2026, holder: "North Records" } },
  externalId: "rel-2041",
  tracks: [{ title: "Night Signals", externalId: "trk-9310" }]
});

3. Add the cover and the masters#

Upload the files in three moves (an upload address, a PUT of the bytes, then …/complete), as the Quickstart shows. Covers are square JPEG or PNG, exactly 3000 × 3000 pixels, in RGB; masters are WAV or FLAC.

Files you already host can be fetched from a public link instead. The import runs in the background: follow it until it says SUCCEEDED or FAILED.

const job = await api.post<{ id: string }>(`/releases/${release.id}/artwork/import`, { url: "https://files.example.com/covers/rel-2041.jpg" });
await api.post(`/tracks/${release.tracks[0]!.id}/audio/import`, { url: "https://files.example.com/masters/trk-9310.wav" });

let state = await api.get<{ status: string; error: string | null }>(`/imports/${job.id}`);
while (state.status === "PENDING" || state.status === "RUNNING") {
  await new Promise((resolve) => setTimeout(resolve, 5000));
  state = await api.get(`/imports/${job.id}`);
}

The link must answer with a Content-Length, and carry no password in it.

4. Splits and cover licences, when they apply#

  • Splits say how the release's money is shared between collaborators: PUT /releases/{id}/split with parties whose shares add up to exactly 100. A track can have its own split (PUT /tracks/{id}/split).
  • Covers: a track marked isCover: true needs a mechanical licence before it goes to the stores. Ask for one with POST /cover-licenses: PURCHASE has Stillmore Group get it for a fixed price, OWN sends the licence you hold (upload it with …/document, then …/document/complete).

5. Check, then submit#

GET /releases/{id}/checks runs everything review would look at. submittable says nothing stops a submission; clean says nothing would hold back an automatic approval. Each check names the field it is about (path), so you can show it next to the field in your own system.

const checks = await api.get<{ submittable: boolean; clean: boolean; checks: Array<{ code: string; severity: string; path: string; message: string }> }>(`/releases/${release.id}/checks`);
if (!checks.submittable) throw new Error(checks.checks.map((check) => `${check.path}: ${check.message}`).join("\n"));
await api.post(`/releases/${release.id}/submit`, {});

The release joins the review queue exactly as one submitted in the workspace does, and release.submitted follows. On an account with automatic approval, a release whose checks are clean is approved without a person and goes straight on to the stores.

6. Answer review#

  • release.changes_requested: the release's review.note and review.issues say what to change. PATCH the release or its tracks, then submit again.
  • Documents: when review.documentsRequested is set, review needs proof of rights, a licence or an agreement. Upload it with POST /releases/{id}/documents, PUT the file, then …/documents/complete.
  • release.rejected: review won't send this release; review.note says why.

7. On its way, then live#

  • release.codes_assigned: the release has its UPC (upc) and its tracks their ISRCs (tracks[].isrc), often before it is live. Save them next to your own ids.
  • release.live and release.store_links_updated: delivery.storeLinks lists the release's pages in the stores. They are kept on the release's smart link too, which is made if there is none.
  • release.provider_rejected: the stores refused it; delivery.refusedReason says why.

8. Share it#

PUT /releases/{id}/smart-link sets the smart link's address, headline and store buttons; notifyMeEnabled lets fans leave their email before release day. GET …/smart-link/stats counts clicks by store.

Later: taking it down#

POST /releases/{id}/takedown with a reason asks Stillmore Group's team to take a live release down from the stores. takedown.requested comes at once, and release.taken_down when it is done.