Deliver DDEX through the API
Everything an SFTP delivery does, over the API. The message and its files go through exactly the same pipeline, and may arrive in either order.
Use this when you already have DDEX ERN files — from a distributor, or exported from another system — and want to deliver them from code rather than over SFTP. To create a release from scratch instead, see Publish a release.
You need catalog_write and media_write.
The flow#
- Upload the XML with requestDeliveryUpload
and a
PUT, exactly as in Upload media. - deliverMessage parses and ingests it, and
returns a
deliveryNamespaceand how many files it is still waiting for. - For each media file: upload it the same way, then deliverFile with the namespace and the path the message used for it.
- When the last file lands, the release goes live on its own.
Steps 2 and 3 can happen in either order. Files delivered before their message are kept, and matched when the message arrives.
1. Upload the message#
mutation {
requestDeliveryUpload(
sha256: "4c1d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d"
byteSize: 18244
) {
alreadyPresent
uploadUrl
requiredHeaders { name value }
}
}curl https://api.schema.fm/graphql \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'EOF'
{
"query": "mutation {\n requestDeliveryUpload(\n sha256: \"4c1d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d\"\n byteSize: 18244\n ) {\n alreadyPresent\n uploadUrl\n requiredHeaders { name value }\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 {
requestDeliveryUpload(
sha256: "4c1d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d"
byteSize: 18244
) {
alreadyPresent
uploadUrl
requiredHeaders { name value }
}
}
`,
}),
});
const { data, errors } = await res.json();PUT the XML to uploadUrl with the required headers, unless
alreadyPresent is true.
2. Deliver the message#
mutation {
deliverMessage(
sha256: "4c1d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d"
fileName: "0196871402293.xml"
) {
messagePk
deliveryNamespace
awaitingFiles
duplicate
}
}curl https://api.schema.fm/graphql \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'EOF'
{
"query": "mutation {\n deliverMessage(\n sha256: \"4c1d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d\"\n fileName: \"0196871402293.xml\"\n ) {\n messagePk\n deliveryNamespace\n awaitingFiles\n duplicate\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 {
deliverMessage(
sha256: "4c1d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d"
fileName: "0196871402293.xml"
) {
messagePk
deliveryNamespace
awaitingFiles
duplicate
}
}
`,
}),
});
const { data, errors } = await res.json();{
"data": {
"deliverMessage": {
"messagePk": 220344,
"deliveryNamespace": "console-4c1d8e7f6a5b",
"awaitingFiles": 3,
"duplicate": false
}
}
}deliveryNamespaceties this message to its files. It is derived from the message's bytes, so delivering the same file again lands in the same place.awaitingFiles: 0means the delivery is already complete — every file it references was already in //SCHEMA.duplicate: truemeans this exact message had already been ingested. Nothing changed, and that is not an error.
A message //SCHEMA cannot read returns an error beginning ERN parse error
or ERN map error, with the details.
3. Deliver each file#
Upload each media file (step 1 again), then land it against the delivery.
relativeUri is the path exactly as the message's <File> element gives it:
mutation {
deliverFile(
deliveryNamespace: "console-4c1d8e7f6a5b"
relativeUri: "resources/A1_Signal_Fire.flac"
sha256: "9f2c4e0d4b1a7c3e8f6d5b2a1c0e9d8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d"
) {
outcome
resourcePk
}
}curl https://api.schema.fm/graphql \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'EOF'
{
"query": "mutation {\n deliverFile(\n deliveryNamespace: \"console-4c1d8e7f6a5b\"\n relativeUri: \"resources/A1_Signal_Fire.flac\"\n sha256: \"9f2c4e0d4b1a7c3e8f6d5b2a1c0e9d8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d\"\n ) {\n outcome\n resourcePk\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 {
deliverFile(
deliveryNamespace: "console-4c1d8e7f6a5b"
relativeUri: "resources/A1_Signal_Fire.flac"
sha256: "9f2c4e0d4b1a7c3e8f6d5b2a1c0e9d8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d"
) {
outcome
resourcePk
}
}
`,
}),
});
const { data, errors } = await res.json();{
"data": {
"deliverFile": { "outcome": "promoted", "resourcePk": 771201 }
}
}outcome | Meaning | Action |
|---|---|---|
promoted | Matched; the resource has its bytes. | None. |
orphan | Stored, but nothing is waiting for it. | Fine if the message is still to come. Otherwise check the namespace and path. |
duplicate | Already delivered at this path. | None. |
hashMismatch | The bytes contradict the checksum in the message. | Deliver the correct file; the resource recovers on its own. |
Don't know the namespace?#
If the message was delivered some other way — over SFTP, or by someone else —
find what it is waiting for with outstandingFiles
and match your file's name against the end of relativeUri:
query {
outstandingFiles {
deliveryNamespace
relativeUri
title
}
}curl https://api.schema.fm/graphql \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'EOF'
{
"query": "query {\n outstandingFiles {\n deliveryNamespace\n relativeUri\n title\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 {
outstandingFiles {
deliveryNamespace
relativeUri
title
}
}
`,
}),
});
const { data, errors } = await res.json();4. Watch it go live#
A delivery stays out of the catalog until every file has arrived and it has cover art. blockedReleases lists what is still held back and why:
query {
blockedReleases {
upc
title
problems
}
}curl https://api.schema.fm/graphql \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'EOF'
{
"query": "query {\n blockedReleases {\n upc\n title\n problems\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 {
blockedReleases {
upc
title
problems
}
}
`,
}),
});
const { data, errors } = await res.json();{
"data": {
"blockedReleases": [
{ "upc": "0196871402293", "title": "Paper Radio", "problems": ["1 of 3 files have not arrived yet"] }
]
}
}When the list no longer includes it, the release is live — find it with
release(upc:). Whether a complete delivery goes live immediately or waits
for review depends on your catalog's activation setting.
If a delivery will never be completed, remove it with discardBlockedRelease.
Check the delivery log#
messages lists recent deliveries — through the API and over SFTP — with their status, and ingestionStats summarises them.
See also#
- Out-of-order delivery — how files and messages find each other.
- Manage SFTP delivery accounts — to let partners deliver directly.