Skip to content
HILLS API v1 Early access

API reference

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.

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.

Ready to integrate?

Request API access and we get back to you within 1–3 business days.