DDEX deliveries
Deliver and validate DDEX ERN batches (4.3 recommended), direct uploads for large ones, and their status.
Deliver and validate DDEX ERN batches (4.3 recommended), direct uploads for large ones, and their status.
Base URL https://app.hillsmusic.com/api/v1. Every request sends an API key: Authorization: Bearer hm_live_… (production) or hm_test_… (sandbox).
- Validate a DDEX message or batch
- Deliver a DDEX batch (ERN 4.3 recommended)
- List deliveries
- Start a direct upload (large batches)
- Where a direct upload stands
- Drop a direct upload
- Commit a direct upload
- Delivery status
Validate a DDEX message or batch
POST /api/v1/ddex/validate · Scope: ddex:validate or ddex:write · Sandbox keys see sandbox data only.
The validation sandbox: an .xml (Content-Type: application/xml) or a .zip; nothing is imported. Every check by stage (pass / warning / error / skipped), what HILLS would create, and the verdict: would_import, would_import_with_warnings, would_be_flagged or would_be_rejected. ddex:write works too.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
member_id |
query | integer | No | The artist/label it’s for (yours when omitted). |
name |
query | string | No | File name for a raw body (default batch.zip, or message.xml for XML). |
Request body
The batch: the zip you deliver to stores (ERN XML + the resources it points at) as the raw body, an .xml on its own, or the multipart field file (with member_id as a field). Content types: application/zip, application/xml, multipart/form-data.
Example request
curl -X POST "https://app.hillsmusic.com/api/v1/ddex/validate?member_id=41" \
-H "Authorization: Bearer $HILLS_API_KEY" \
-H "Content-Type: application/zip" \
--data-binary @batch.zip
Response 200
{
"data": {
"ern_version": "4.3",
"verdict": "would_be_rejected",
"summary": {
"messages": 1,
"releases": 1,
"pass": 41,
"warnings": 1,
"errors": 2,
"skipped": 1
},
"checks": [
{
"stage": "release",
"label": "Label",
"status": "error",
"message": "ReleaseLabelReference is required…",
"field": "Release/ReleaseLabelReference"
}
]
}
}
Errors
| Status | When |
|---|---|
| 400 | invalid: a parameter is wrong; field names it. |
| 401 | unauthenticated, invalid_key, key_revoked or key_expired. |
| 403 | insufficient_scope: the key lacks the scope; scope names it. account_inactive or api_not_in_plan. |
| 413 | too_large: Larger than validation takes; deliver it with POST /ddex. |
| 429 | busy: Too many dry runs at once; try again shortly. rate_limited: wait Retry-After seconds. |
Deliver a DDEX batch (ERN 4.3 recommended)
POST /api/v1/ddex · Scope: ddex:write · Sandbox keys see sandbox data only.
Send the zip you deliver to stores: ERN NewReleaseMessage XML(s) with the audio, cover and videos they point at. As the raw body (Content-Type: application/zip, up to 20 GB) or the multipart field file (up to 512 MB). Each release becomes a draft on member_id (your own catalog when omitted); a UPC already there updates its draft; takedowns and updates of live releases wait for staff. ERN 4.3 (namespace http://ddex.net/xml/ern/43) is recommended; 4.2, 4.1 and 3.8.x (3.8.2, 3.8.1, 3.8) and 3.7 are accepted and read the same way (the report warns that the version is legacy); other versions are rejected. 202 with the delivery id — poll GET /ddex/{id}.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
member_id |
query | integer | No | The artist/label it’s for (yours when omitted). |
name |
query | string | No | File name for a raw body (default batch.zip). |
Request body
The batch: the zip you deliver to stores (ERN XML + the resources it points at) as the raw body, an .xml on its own, or the multipart field file (with member_id as a field). Content types: application/zip, application/xml, multipart/form-data.
Example request
curl -X POST "https://app.hillsmusic.com/api/v1/ddex?member_id=41" \
-H "Authorization: Bearer $HILLS_API_KEY" \
-H "Content-Type: application/zip" \
--data-binary @batch.zip
Response 202
{
"data": {
"id": 311,
"status": "queued",
"environment": "production",
"status_url": "/api/v1/ddex/311"
}
}
Errors
| Status | When |
|---|---|
| 400 | invalid: a parameter is wrong; field names it. |
| 401 | unauthenticated, invalid_key, key_revoked or key_expired. |
| 403 | insufficient_scope: the key lacks the scope; scope names it. account_inactive or api_not_in_plan. |
| 413 | too_large: Over 20 GB (raw body) or 512 MB (multipart). |
| 429 | rate_limited: wait Retry-After seconds. |
List deliveries
GET /api/v1/ddex · Scope: ddex:write or ddex:validate · Sandbox keys see sandbox data only.
Your last 100 DDEX deliveries in the key’s environment, newest first (uploads not committed yet are left out).
Example request
curl "https://app.hillsmusic.com/api/v1/ddex" \
-H "Authorization: Bearer $HILLS_API_KEY"
Response 200
{
"data": [
{
"id": 311,
"member_id": 41,
"environment": "production",
"batch_name": "batch-0001.zip",
"status": "done",
"error": null,
"message_id": "MSG-2026-0001",
"ern_version": "4.3",
"message_count": 1,
"release_count": 1,
"track_count": 12,
"created_count": 1,
"updated_count": 0,
"flagged_count": 0,
"left_out_count": 0,
"needs_staff": false,
"release_ids": [
912
],
"created_at": "2026-10-07T09:00:00Z",
"started_at": "2026-10-07T09:00:02Z",
"finished_at": "2026-10-07T09:01:10Z"
}
]
}
Errors
| Status | When |
|---|---|
| 401 | unauthenticated, invalid_key, key_revoked or key_expired. |
| 403 | insufficient_scope: the key lacks the scope; scope names it. account_inactive or api_not_in_plan. |
| 429 | rate_limited: wait Retry-After seconds. |
Start a direct upload (large batches)
POST /api/v1/ddex/uploads · Scope: ddex:write · Sandbox keys see sandbox data only.
Recommended for big zips: the file goes straight to storage instead of through the API. Send the file name and its size in bytes (up to 20 GB). Up to 64 MB you get one URL to PUT the whole file to; above that, a part size and one URL per part: PUT each slice of part_size bytes (the last one shorter) to its URL, in any order and in parallel. URLs work for 6 hours; GET /ddex/uploads/{id} gives fresh ones for the parts still missing. Then POST /ddex/uploads/{id}/commit. DELETE /ddex/uploads/{id} drops an upload you won’t finish; uncommitted uploads are removed after 7 days.
Request body
JSON.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | The file name (.zip or .xml). |
size |
integer | Yes | The file’s size in bytes. |
member_id |
integer | No | The artist/label it’s for (yours when omitted). |
Example request
curl -X POST "https://app.hillsmusic.com/api/v1/ddex/uploads" \
-H "Authorization: Bearer $HILLS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"batch-0001.zip","size":1073741824,"member_id":41}'
Response 201
{
"data": {
"id": 312,
"status": "uploading",
"environment": "production",
"name": "batch-0001.zip",
"size": 1073741824,
"expires_in": 21600,
"upload": {
"method": "multipart",
"url": null,
"part_size": 16777216,
"parts": [
{
"part_number": 1,
"url": "https://…"
}
]
},
"commit_url": "/api/v1/ddex/uploads/312/commit"
}
}
Errors
| Status | When |
|---|---|
| 400 | invalid: a parameter is wrong; field names it. |
| 401 | unauthenticated, invalid_key, key_revoked or key_expired. |
| 403 | insufficient_scope: the key lacks the scope; scope names it. account_inactive or api_not_in_plan. |
| 429 | rate_limited: wait Retry-After seconds. |
Where a direct upload stands
GET /api/v1/ddex/uploads/{id} · Scope: ddex:write · Sandbox keys see sandbox data only.
An upload not committed yet: fresh URLs for the parts still missing (the first ones expire after 6 hours), received_parts and parts_total. Once committed: its delivery id and status_url.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | integer | Yes | The upload id. |
Example request
curl "https://app.hillsmusic.com/api/v1/ddex/uploads/312" \
-H "Authorization: Bearer $HILLS_API_KEY"
Response 200
{
"data": {
"id": 312,
"status": "uploading",
"environment": "production",
"name": "batch-0001.zip",
"size": 1073741824,
"expires_in": 21600,
"upload": {
"method": "multipart",
"url": null,
"part_size": 16777216,
"parts": [
{
"part_number": 7,
"url": "https://…"
}
]
},
"commit_url": "/api/v1/ddex/uploads/312/commit",
"received_parts": 63,
"parts_total": 64
}
}
Errors
| Status | When |
|---|---|
| 401 | unauthenticated, invalid_key, key_revoked or key_expired. |
| 403 | insufficient_scope: the key lacks the scope; scope names it. account_inactive or api_not_in_plan. |
| 404 | not_found: no such thing, or not yours (or not in the key’s environment). |
| 429 | rate_limited: wait Retry-After seconds. |
Drop a direct upload
DELETE /api/v1/ddex/uploads/{id} · Scope: ddex:write · Sandbox keys see sandbox data only.
Drops an upload you won’t finish, with the parts already sent. 409 once it was committed.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | integer | Yes | The upload id. |
Example request
curl -X DELETE "https://app.hillsmusic.com/api/v1/ddex/uploads/312" \
-H "Authorization: Bearer $HILLS_API_KEY"
Response 200
{
"data": {
"id": 312,
"deleted": true
}
}
Errors
| Status | When |
|---|---|
| 401 | unauthenticated, invalid_key, key_revoked or key_expired. |
| 403 | insufficient_scope: the key lacks the scope; scope names it. account_inactive or api_not_in_plan. |
| 404 | not_found: no such thing, or not yours (or not in the key’s environment). |
| 409 | already_committed: Committed: it can’t be dropped. |
| 429 | rate_limited: wait Retry-After seconds. |
Commit a direct upload
POST /api/v1/ddex/uploads/{id}/commit · Scope: ddex:write · Sandbox keys see sandbox data only.
Checks that every part arrived with the right size and queues the batch exactly like POST /ddex. 409 upload_incomplete lists the missing and wrong-size parts (upload them, then commit again). 202 with the delivery id; poll GET /ddex/{id}.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | integer | Yes | The upload id. |
Example request
curl -X POST "https://app.hillsmusic.com/api/v1/ddex/uploads/312/commit" \
-H "Authorization: Bearer $HILLS_API_KEY"
Response 202
{
"data": {
"id": 312,
"status": "queued",
"environment": "production",
"status_url": "/api/v1/ddex/312"
}
}
Errors
| Status | When |
|---|---|
| 401 | unauthenticated, invalid_key, key_revoked or key_expired. |
| 403 | insufficient_scope: the key lacks the scope; scope names it. account_inactive or api_not_in_plan. |
| 404 | not_found: no such thing, or not yours (or not in the key’s environment). |
| 409 | upload_incomplete: Parts missing or of the wrong size (missing, wrong_size list them). already_committed: Committed before (status_url points at the delivery). commit_in_progress: Another commit of this upload is running. |
| 429 | rate_limited: wait Retry-After seconds. |
Delivery status
GET /api/v1/ddex/{id} · Scope: ddex:write or ddex:validate · Sandbox keys see sandbox data only.
queued → parsing → validating → importing → done / partial / failed, with one item per release (created, updated, flagged for staff, left out, test) and every error with its DDEX field. GET /ddex lists your last 100.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | integer | Yes | The delivery id (from POST /ddex or the commit). |
Example request
curl "https://app.hillsmusic.com/api/v1/ddex/311" \
-H "Authorization: Bearer $HILLS_API_KEY"
Response 200
{
"data": {
"id": 311,
"member_id": 41,
"environment": "production",
"status": "partial",
"message_id": "MSG-2026-0001",
"ern_version": "4.3",
"created_count": 1,
"left_out_count": 1,
"items": [
{
"ref": "R0",
"title": "Midnight Run",
"upc": "0602600000017",
"outcome": "created",
"release_id": 912,
"errors": []
}
]
}
}
Errors
| Status | When |
|---|---|
| 401 | unauthenticated, invalid_key, key_revoked or key_expired. |
| 403 | insufficient_scope: the key lacks the scope; scope names it. account_inactive or api_not_in_plan. |
| 404 | not_found: no such thing, or not yours (or not in the key’s environment). |
| 429 | rate_limited: wait Retry-After seconds. |