//SCHEMADocs
Console
How-to guides

Deliver DDEX over SFTP

Every organization gets its own SFTP endpoint. Drop a delivery directory into it and //SCHEMA ingests the message and matches its media, in whatever order they arrive.

This is the path a distributor or label already knows how to use: put the ERN XML and its audio and artwork in a directory, upload the directory, done. Nothing calls an API, and nothing has to be sequenced. To deliver the same files from code instead, see Deliver DDEX through the API.

Your organization's endpoint#

Every organization gets its own SFTP endpoint — its own reserved IP address and its own SSH host key. The address does not change for as long as you have the endpoint, so partners can allow it through their firewalls. It is not a shared host that partners are partitioned inside, which matters for two reasons: usernames are yours to choose (two organizations may both have a lanternfish without colliding), and the host key a partner pins belongs to you alone.

catalog gives you the address and port to hand out:

Request
query {
  catalog { sftpHost sftpPort }
}
Response
{ "data": { "catalog": { "sftpHost": "203.0.113.24", "sftpPort": 22 } } }

A null sftpHost means the endpoint has not been provisioned yet. It is created the first time you issue a delivery account, so issue one and check again in a few minutes.

Get a login#

Deliveries are authenticated as a delivery account — one per partner, not one per organization — using either an SSH key or a password. Issuing them is covered in Manage SFTP delivery accounts; you need config_write.

Which account uploaded a file decides which catalog the delivery lands in, so an organization with a production and a sandbox catalog gives out separate accounts rather than separate directories.

One directory per delivery#

Every account is chrooted to its own home, with a single writable directory called upload/. Inside it, each delivery is one directory, holding the ERN XML and every file it references:

upload/
└── 20260917_0196871402293/        ← the delivery, named by you
    ├── 0196871402293.xml
    ├── resources/
    │   ├── A1_Signal_Fire.flac
    │   └── A2_Nightcall_Ferry.flac
    └── images/
        └── front.jpg

Two rules about that layout, and both are load-bearing:

The directory name is the delivery namespace. It is what ties a message to its files, so one delivery per directory, and a name you do not reuse — a date and the UPC, a batch id from your own system, anything unique to you. Reusing a name can let a leftover file from the earlier batch satisfy an identically named path in the later one.

Paths inside the directory must match the paths in the XML. A <File> element giving resources/A1_Signal_Fire.flac is matched against exactly that path below the delivery directory. Subdirectories are fine and are part of the path; the delivery directory itself is not.

done is reserved — see below — so a delivery cannot be called that.

Upload it#

sftp -P 22 [email protected]
sftp> put -r 20260917_0196871402293 upload/

Or non-interactively, which is what a nightly job usually wants:

rsync -av -e 'ssh -p 22' \
  20260917_0196871402293 \
  [email protected]:upload/

Each file is picked up when your client closes it, not when it appears, so a partly written file is never read and there is no need to upload to a temporary name and rename. There is also no ordering requirement: the XML may go up before its media or after it, in the same session or a day later, and the delivery converges on the same result either way. See Out-of-order delivery for what happens in between.

These file types are ingested:

Message.xml
Audio.wav .flac .mp3 .aiff .aif .m4a .ogg .aac
Image.jpg .jpeg .png .gif .webp .tiff .tif
Video.mp4 .mov .avi .mkv .m4v

Anything else — a checksum manifest, a .DS_Store, a PDF one-sheet — is set aside rather than ingested, and does not hold the delivery up.

What the directory tells you#

Watch the delivery directory to see how far it has got. Each file is moved to upload/done/<delivery>/ once it has been handled, and when the last one goes the now-empty delivery directory is removed.

So:

  • The delivery directory is gone. Every file was handled. That is the filesystem's version of "complete".
  • Files are still sitting in it. Either they have not been processed yet, or processing failed and they are being retried. A file that keeps failing stays where it is rather than being silently discarded.
  • Files appear under done/. They were handled. Unknown file types land here too — being set aside is not the same as being ingested.

Re-uploading a delivery is always safe. //SCHEMA deduplicates on the message and on file content, so a repeated delivery is recorded as a duplicate and changes nothing.

Check it landed#

messages lists recent deliveries, from SFTP and the API alike, with the status of each:

Request
query {
  messages(limit: 5) {
    fileName
    ddexMessageId
    status
    failureReason
    dateReceived
  }
}
Response
{
  "data": {
    "messages": [
      {
        "fileName": "0196871402293.xml",
        "ddexMessageId": "LANTERNFISH_20260917_0196871402293",
        "status": "partial",
        "failureReason": null,
        "dateReceived": "2026-09-17T02:14:09+00:00"
      }
    ]
  }
}

partial means the message is in and //SCHEMA is still waiting for files — the usual state a few seconds into an upload. It becomes ingested when the last one lands; failed means the XML could not be read, and failureReason says why.

A delivery stays out of the catalog until every file it references has arrived and it has cover art. blockedReleases says what is still held back and why, and outstandingFiles names the exact paths still being waited on — which is the fastest way to find a path that does not match the XML:

Request
query {
  outstandingFiles {
    deliveryNamespace
    relativeUri
    title
  }
}

When nothing happens at all#

SymptomCause
Connection refused, or the host does not resolveThe endpoint is not provisioned. Check sftpHost is not null.
Permission denied writing to the login directoryEverything is uploaded inside upload/; the directory above it is not writable, as SSH requires.
Files stay put and never reach done/The account was revoked. Files are left exactly where they landed rather than being guessed into a catalog.
The message ingests, the release never appearsA file is missing or its path does not match the XML. Check outstandingFiles.
The files vanish but no release appearsCheck messages for a parse or mapping error on that delivery.

See also#