Stream media
Media is never public. Every file has encodings — AAC for streaming, a preview clip, lossless where your plan includes it — and you mint a signed URL for the one you want.
Choose an encoding#
Every audio master you hold is measured and encoded moments after it lands. List what a track has with encodings:
query {
release(upc: "0196871402293") {
resources(kind: AUDIO) {
resourcePk
title
encodings {
encoding
clip { startMs durationMs }
original
codec
bitrateKbps
lossless
sha256
}
}
}
}curl https://api.schema.fm/graphql \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'EOF'
{
"query": "query {\n release(upc: \"0196871402293\") {\n resources(kind: AUDIO) {\n resourcePk\n title\n encodings {\n encoding\n clip { startMs durationMs }\n original\n codec\n bitrateKbps\n lossless\n sha256\n }\n }\n }\n}"
}
EOFconst res = await fetch("https://api.schema.fm/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SCHEMA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
query {
release(upc: "0196871402293") {
resources(kind: AUDIO) {
resourcePk
title
encodings {
encoding
clip { startMs durationMs }
original
codec
bitrateKbps
lossless
sha256
}
}
}
}
`,
}),
});
const { data, errors } = await res.json();{
"data": {
"release": {
"resources": [
{
"resourcePk": 771201,
"title": "Signal Fire",
"encodings": [
{
"encoding": "original",
"clip": null,
"original": true,
"codec": "pcm_s24le",
"bitrateKbps": 4608,
"lossless": true,
"sha256": "9f2c4e0d1a7b3c58e2f6d9a0b4c7e1f3a5d8c2b6e9f0a3d7c1b5e8f2a4d6c9b0"
},
{
"encoding": "aac-256-v1",
"clip": null,
"original": false,
"codec": "aac",
"bitrateKbps": 256,
"lossless": false,
"sha256": "3e7a1c9f5b2d8e4a0c6f1b3d7e9a2c5f8b0d4e6a1c3f5b7d9e2a4c6f8b1d3e5a"
},
{
"encoding": "aac-128-v1",
"clip": null,
"original": false,
"codec": "aac",
"bitrateKbps": 128,
"lossless": false,
"sha256": "b4d6f8a0c2e4b6d8f0a2c4e6b8d0f2a4c6e8b0d2f4a6c8e0b2d4f6a8c0e2b4d6"
},
{
"encoding": "aac-128-v1",
"clip": { "startMs": 30000, "durationMs": 30000 },
"original": false,
"codec": "aac",
"bitrateKbps": 128,
"lossless": false,
"sha256": "c1e3a5b7d9f1c3e5a7b9d1f3c5e7a9b1d3f5c7e9a1b3d5f7c9e1a3b5d7f9c1e3"
}
]
}
]
}
}
}| Encoding | Made | What it is for |
|---|---|---|
aac-256 | for every master | The default stream. |
aac-128 | for every master | Cellular, and auditioning. |
aac-128, clip: true | for every master | A 30-second preview. |
flac | for every master an organization with lossless media holds | The lossless pull. |
mp3-128, mp3-320, mp3-v0, wav, alac | on request | Download stores, legacy players. |
original | — | The bytes you delivered. |
encodingProfiles lists them all — read it rather
than hardcoding names. A family (aac-256) follows the newest version; a
pinned name (aac-256-v1) never changes.
Get a signed URL#
Name the encoding to url:
query {
release(upc: "0196871402293") {
resources(kind: AUDIO) {
title
url(encoding: "aac-256", ttlSeconds: 600)
}
}
}curl https://api.schema.fm/graphql \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'EOF'
{
"query": "query {\n release(upc: \"0196871402293\") {\n resources(kind: AUDIO) {\n title\n url(encoding: \"aac-256\", ttlSeconds: 600)\n }\n }\n}"
}
EOFconst res = await fetch("https://api.schema.fm/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SCHEMA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
query {
release(upc: "0196871402293") {
resources(kind: AUDIO) {
title
url(encoding: "aac-256", ttlSeconds: 600)
}
}
}
`,
}),
});
const { data, errors } = await res.json();{
"data": {
"release": {
"resources": [
{
"title": "Signal Fire",
"url": "https://media.schema.fm/sha256/3e/7a/3e7a1c9f…?X-Amz-Expires=600&X-Amz-Signature=…"
}
]
}
}
}The URL is an ordinary HTTPS link. GET it from your server and pull the file
into your own pipeline, then check it against the encoding's sha256. Pass
download: true and a browser saves it as Artist - Title.m4a instead of
playing it.
Artwork has no encodings: ask for url(encoding: "original") on
resources(kind: IMAGE), or for a cover at one of your organization's sizes
with artwork(size:) — see Size cover art.
Which key may pull what#
The scope depends on what the file is, not on whether it is the original:
| Scope | Reaches |
|---|---|
media_preview | clips, and AAC 128 |
media_read | everything media_preview does, every other lossy encoding, and artwork |
media_lossless | FLAC, WAV, ALAC, and a lossless original — organizations with lossless media only: every enterprise organization, and others by arrangement |
So a DSP's editorial tool can audition a whole catalog with a
media_preview key that cannot pull a master-quality file. Lossless is
checked against your organization's plan every time a URL is minted, not
when the key was made. A master not yet analyzed counts as lossless.
Every URL minted is logged — which key, which file, when — so a leaked link can be traced. The release does not have to be live: if the file is here, you can pull it, and what you do with an unreleased track is governed by your agreement with the label.
Play a preview in a browser#
A signed URL works in an <audio> element, but keep the API key on your
server and hand the browser only the preview:
// Your backend: the browser asks to audition a track, you return a fresh URL.
app.get("/api/preview/:upc/:sequence", async (req, res) => {
const { data } = await schema(
`query ($upc: String!) {
release(upc: $upc) {
resources(kind: AUDIO) { sequence url(encoding: "aac-128", clip: true) }
}
}`,
{ upc: req.params.upc },
);
const track = data.release?.resources.find((r) => r.sequence === Number(req.params.sequence));
if (!track?.url) return res.sendStatus(404);
res.json({ url: track.url });
});Full-length playback to listeners — tokens, packaging, DRM — is a separate surface, not a signed URL handed to a page.
Choose a lifetime#
ttlSeconds | Result |
|---|---|
| omitted | 300 seconds (5 minutes) |
| 1–3600 | as requested |
| above 3600 | capped at 3600 (1 hour) |
| 0 | error |
Short is better: a leaked URL is worthless once it expires. Mint URLs when you are about to fetch, not when you render a page of 200 tracks.
When url is null#
A resource can exist before its file does. While state is incoming, url
is null — not an error.
Any other null comes with an error whose extensions.code says why:
ENCODING_PENDING (it is being made, usually within a minute of the file
landing), ENCODING_NOT_REQUESTED, ENCODING_NOT_AVAILABLE,
LOSSLESS_NOT_ENABLED and others — see
Encodings and media URLs.
Ask for an encoding made on request#
mutation {
requestEncoding(resourcePk: 771201, encoding: "mp3-320") {
encoding
status
}
}curl https://api.schema.fm/graphql \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'EOF'
{
"query": "mutation {\n requestEncoding(resourcePk: 771201, encoding: \"mp3-320\") {\n encoding\n status\n }\n}"
}
EOFconst res = await fetch("https://api.schema.fm/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SCHEMA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
mutation {
requestEncoding(resourcePk: 771201, encoding: "mp3-320") {
encoding
status
}
}
`,
}),
});
const { data, errors } = await res.json();{
"data": {
"requestEncoding": {
"encoding": "mp3-320-v1",
"status": "PENDING"
}
}
}Subscribe to resource.encoded and mint the URL when it arrives; nothing needs to poll. Encodings made on request that nobody fetches for 30 days are removed, and can be requested again.
Download the original DDEX file#
Every delivered message keeps its XML exactly as received. Message.sourceUrl signs a link to it:
query {
messages(limit: 1) {
fileName
status
sourceUrl
}
}curl https://api.schema.fm/graphql \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'EOF'
{
"query": "query {\n messages(limit: 1) {\n fileName\n status\n sourceUrl\n }\n}"
}
EOFconst res = await fetch("https://api.schema.fm/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SCHEMA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
query {
messages(limit: 1) {
fileName
status
sourceUrl
}
}
`,
}),
});
const { data, errors } = await res.json();Raw deliveries contain commercial terms, so this needs media_read.
Use the file's properties#
technical is measured by //SCHEMA from the file itself, not
asserted by the sender: codec, sample rate, bit depth, durationMs, whether
it is lossless, and EBU R128 loudness (integratedLufs, truePeakDbtp).
Compare durationMs with the delivered durationSeconds to catch a truncated
or wrong file. It is null until the file has been analyzed;
resource.analyzed says when.