//SCHEMADocs
Console
Tutorials

Publish a release

In this tutorial you will create a one-track single entirely through the API, from an empty draft to a live release you can read back with release(upc:).

You will need:

  • An API key with catalog_read, catalog_write and media_write. The Quickstart shows how to create one.
  • An audio file (track.flac) and a square cover image (cover.jpg).
  • curl, and sha256sum (on macOS, shasum -a 256).

The examples use the GraphQL tab; switch any of them to cURL to paste straight into a terminal.

1. Start a draft#

A release you author starts as a draft: private to your catalog, not listed by releases, and editable until you publish it.

Request
mutation {
  createRelease(input: { title: "Harbour Lights", releaseType: "Single" }) {
    releasePk
    state
  }
}
Response
{ "data": { "createRelease": { "releasePk": 48290, "state": "draft" } } }

Keep the releasePk — every later step refers to it. We will add the UPC at the end, to see what publishing says when something is missing.

2. Credit the artist#

Create the artist as a party, then credit them on the release as its MainArtist:

Request
mutation {
  createParty(input: { name: "Halfway Ghosts" }) {
    partyPk
  }
}
Response
{ "data": { "createParty": { "partyPk": 90450 } } }
Request
mutation {
  creditRelease(releasePk: 48290, input: { partyPk: 90450, role: "MainArtist" }) {
    contributors { party { name } role }
  }
}
Response
{
  "data": {
    "creditRelease": {
      "contributors": [{ "party": { "name": "Halfway Ghosts" }, "role": "MainArtist" }]
    }
  }
}

3. Name the label and the rights holders#

The label is a party too — not a credit — so create it and set it with labels. The ℗ notice (who owns the recording) and the © notice (who owns the song) go in the same update:

Request
mutation {
  createParty(input: { name: "Lanternfish Records" }) {
    partyPk
  }
}
Response
{ "data": { "createParty": { "partyPk": 90211 } } }
Request
mutation {
  updateRelease(releasePk: 48290, input: {
    labels: [{ partyPk: 90211 }]
    plines: [{ text: "(P) 2026 Lanternfish Records", year: "2026" }]
    clines: [{ text: "(C) 2026 Halfway Ghosts" }]
  }) {
    labels { name }
    plines { value }
    clines { value }
  }
}
Response
{
  "data": {
    "updateRelease": {
      "labels": [{ "name": "Lanternfish Records" }],
      "plines": [{ "value": "(P) 2026 Lanternfish Records" }],
      "clines": [{ "value": "(C) 2026 Halfway Ghosts" }]
    }
  }
}

Each list replaces what was there, so sending clines: [] later removes the notice. None of the three is required to publish — a netlabel release may carry no notices at all — but stores display them when they are present.

4. Add the track and the cover#

Both are resources. The track is a MusicalWorkSoundRecording; the cover is a FrontCoverImage from the ImageType vocabulary.

Request
mutation {
  track: addResource(input: {
    releasePk: 48290
    resourceType: "MusicalWorkSoundRecording"
    title: "Harbour Lights"
    isrc: "JPTO09404920"
    durationSeconds: 203
  }) { resourcePk state }
  cover: addResource(input: {
    releasePk: 48290
    resourceType: "FrontCoverImage"
    resourceTypeKind: "ImageType"
  }) { resourcePk state }
}
Response
{
  "data": {
    "track": { "resourcePk": 771340, "state": "incoming" },
    "cover": { "resourcePk": 771341, "state": "incoming" }
  }
}

Aliases (track:, cover:) let one request call the same mutation twice. Both resources are incoming: they exist, but have no bytes yet.

5. Upload the audio#

Files go straight to storage, never through the API. First, hash the file and measure it:

sha256sum track.flac     # 9f2c4e0d…a4b3c2d  track.flac
wc -c < track.flac       # 48213377

Ask for permission to upload exactly those bytes:

Request
mutation {
  requestUpload(
    resourcePk: 771340
    sha256: "9f2c4e0d4b1a7c3e8f6d5b2a1c0e9d8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d"
    byteSize: 48213377
  ) {
    alreadyPresent
    uploadUrl
    requiredHeaders { name value }
  }
}
Response
{
  "data": {
    "requestUpload": {
      "alreadyPresent": false,
      "uploadUrl": "https://media.schema.fm/sha256/9f/2c/9f2c4e0d…&X-Amz-Signature=…",
      "requiredHeaders": [
        { "name": "x-amz-checksum-sha256", "value": "nyxODUsafD6PbVsqHA6djzp....Mi0=" }
      ]
    }
  }
}

PUT the file to uploadUrl, sending every header in requiredHeaders exactly as given:

curl -X PUT --upload-file track.flac \
  -H "x-amz-checksum-sha256: nyxODUsafD6PbVsqHA6djzp....Mi0=" \
  "https://media.schema.fm/sha256/9f/2c/9f2c4e0d…&X-Amz-Signature=…"

Storage checks the bytes against the hash you declared and refuses anything else. Then tell the API the upload is done:

Request
mutation {
  completeUpload(
    resourcePk: 771340
    sha256: "9f2c4e0d4b1a7c3e8f6d5b2a1c0e9d8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d"
  ) { state }
}
Response
{ "data": { "completeUpload": { "state": "stored" } } }

6. Upload the cover#

Repeat step 5 for cover.jpg with the cover's resourcePk (771341).

If //SCHEMA already holds that exact image — covers are often reused across a single and its album — requestUpload answers alreadyPresent: true and no uploadUrl. Skip the PUT and go straight to completeUpload.

7. Try to publish#

Request
mutation {
  publishRelease(releasePk: 48290) {
    published
    problems
  }
}
Response
{
  "data": {
    "publishRelease": {
      "published": false,
      "problems": ["a UPC or GRid is required to publish"]
    }
  }
}

Publishing checks everything at once and reports every problem, not just the first. A release needs a UPC or GRid, at least one resource, bytes behind every resource, and a front cover.

8. Add the UPC and publish#

Request
mutation {
  updateRelease(releasePk: 48290, input: { upc: "0196871555012" }) {
    upc
  }
  publishRelease(releasePk: 48290) {
    published
    problems
    release { state availableFrom }
  }
}
Response
{
  "data": {
    "updateRelease": { "upc": "0196871555012" },
    "publishRelease": {
      "published": true,
      "problems": [],
      "release": { "state": "active", "availableFrom": "2026-09-18T10:22:41.302118+00:00" }
    }
  }
}

Mutations in one request run in order, so the UPC is in place before the publish checks for it.

9. Read it back#

The release is now part of your catalog, exactly like a delivered one:

Request
query {
  release(upc: "0196871555012") {
    title
    state
    contributors { party { name } role }
    labels { name }
    resources { title resourceType state }
  }
}
Response
{
  "data": {
    "release": {
      "title": "Harbour Lights",
      "state": "active",
      "contributors": [{ "party": { "name": "Halfway Ghosts" }, "role": "MainArtist" }],
      "labels": [{ "name": "Lanternfish Records" }],
      "resources": [
        { "title": "Harbour Lights", "resourceType": "MusicalWorkSoundRecording", "state": "stored" },
        { "title": null, "resourceType": "FrontCoverImage", "state": "stored" }
      ]
    }
  }
}

What you have learned#

  • Authoring is draft → resources → uploads → publish.
  • Bytes go directly to storage, bound to the SHA-256 you declare.
  • Publishing reports every problem at once, as data.

Next#