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#
| Status | What is happening | Events |
|---|---|---|
DRAFT | You build the release: details, tracks, files | |
IN_REVIEW | Stillmore Group's review has it | release.submitted |
CHANGES_REQUESTED | Review sent it back with a note | release.changes_requested |
APPROVED | Review passed it | release.approved |
SENT_TO_STORES | On its way to the stores | release.sent, release.codes_assigned |
LIVE | In the stores | release.live, release.store_links_updated |
REFUSED_BY_STORES | The stores refused it | release.provider_rejected |
REJECTED | Review won't send it | release.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}/splitwith parties whose shares add up to exactly 100. A track can have its own split (PUT /tracks/{id}/split). - Covers: a track marked
isCover: trueneeds a mechanical licence before it goes to the stores. Ask for one withPOST /cover-licenses:PURCHASEhas Stillmore Group get it for a fixed price,OWNsends 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'sreview.noteandreview.issuessay what to change.PATCHthe release or its tracks, then submit again.- Documents: when
review.documentsRequestedis set, review needs proof of rights, a licence or an agreement. Upload it withPOST /releases/{id}/documents,PUTthe file, then…/documents/complete. release.rejected: review won't send this release;review.notesays 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.liveandrelease.store_links_updated:delivery.storeLinkslists 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.refusedReasonsays 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.