Soundtracking (Beta)
This API is in beta. Request and response shapes may still change before general availability.
The Soundtracking API scores a video with music from the Epidemic Sound catalog. You submit a video file and choose what you get back: the video with the soundtrack mixed in (MP4), or the soundtrack on its own as an audio file (MP3 or WAV).
When to use this
The Soundtracking API is built for video-to-music automation with zero manual selection — good for:
- A product that lets end users upload or publish raw video clips with no built-in editor for adding music.
- A batch or backend pipeline that scores a queue of videos unattended.
- A single-action "add music" step alongside a manual editing flow, for users who don't want to browse the catalog themselves.
Already have a track-picking UI powered by track search? The Soundtracking API complements that flow for automated cases — it's not meant to replace a UI where users are actively choosing tracks themselves.
Prerequisites
- API Key authentication — The Soundtracking API requires API Key authentication.
- AI credits — The Soundtracking API runs on the AI credits of your workspace. If creating a job returns
402, your workspace has no credits left or no access to AI credits. Contact your Epidemic Sound partner manager.
How it works
Scoring and rendering are asynchronous. The API models this as a job: you create it in one call, upload your video, and poll until it finishes. A 30-second video takes about a minute to process, and longer videos take proportionally longer.
Step 1 — Create the job. Send the video's metadata and any creative direction. The API hands back a job and a signed upload URL. No video bytes pass through this call.
Step 2 — Upload the video. PUT the whole file to upload.url. There is no separate "upload complete" call — the file landing is what triggers processing.
Step 3 — Poll until done. GET the job on a 5-second interval or slower. When the status reaches COMPLETED, the response carries a signed URL to the finished file and the list of library items used.
Authentication
All Soundtracking requests use API Key authentication. Include your API key and a stable per-user identifier on every request to the Soundtracking API.
Authorization: Bearer {api-key}
x-partner-user-id: {user-id}
Content-Type: application/json
The upload step is the exception — the upload.url carries its own credentials.
Create a job
POST /v1/soundtracking/jobs — see Create a soundtrack job in the API reference.
curl -X POST 'https://api.epidemicsound.com/v1/soundtracking/jobs' \
-H 'Authorization: Bearer your-api-key' \
-H 'x-partner-user-id: user-abc-123' \
-H 'Content-Type: application/json' \
-d '{
"filename": "product-launch.mp4",
"contentType": "video/mp4",
"sizeBytes": 42000000,
"brief": "upbeat, corporate, no vocals",
"outputFormat": "MP4"
}'
Request body
| Field | Required | Type | Default | Description |
|---|---|---|---|---|
filename | Yes | string | — | Filename for the upload. Sets the storage file extension. 1–512 characters. |
contentType | Yes | string | — | MIME type of the upload. Must be a video/* type, e.g. video/mp4, video/quicktime. |
sizeBytes | Yes | integer | — | File size in bytes. Maximum 629,145,600 (600 MB). |
brief | No | string | — | Free-text creative direction — mood, genre, style references. 1–2000 characters. |
audioTypes | No | string[] | ["MUSIC"] | What the run adds to the video. Currently only MUSIC (library recordings) is supported. |
outputFormat | No | string | MP4 | Output format. MP4 includes the video; MP3 and WAV are audio-only. |
sourceAudio | No | string | MUTED | How to handle existing audio on the video. MUTED discards it; UNMUTED keeps it and mixes the soundtrack underneath. |
contentType is stamped on the upload session when it is created, so a non-video/* value is rejected immediately before any bytes move. The type names the container format, not the codec, so passing a valid type is not a guarantee the video can be decoded. Codec support is settled after upload.
sizeBytes is declared to storage at create time — an upload that does not match the declared size is refused as it arrives. A value above 600 MB is rejected by this call before any bytes move.
brief is a prompt, not a filter. It guides music selection for the whole run but cannot guarantee a specific track or genre. Omit it to let the API decide from the video content alone.
Fields not listed above are ignored, so a misspelled field name is dropped and the request still succeeds.
Response — 202 Accepted
{
"job": {
"id": "9c3b1a2e-...",
"status": "AWAITING_FILE_UPLOAD",
"createdAt": "2026-09-27T17:00:00Z",
"updatedAt": "2026-09-27T17:00:00Z",
"brief": "upbeat, corporate, no vocals",
"audioTypes": ["MUSIC"],
"sourceAudio": "MUTED"
},
"upload": {
"url": "https://example.com/storage/...",
"expiresAt": "2026-09-27T18:00:00Z"
}
}
job contains every ID you need. If you lose this response, replay the create call with the same Idempotency-Key to retrieve it.
upload is where you send the video. upload.expiresAt is when that URL stops working — allow enough lead time for the transfer to complete before the deadline.
Creating a job checks your workspace's credit balance and returns 402 if it cannot cover the job. See Credits.
Upload the video
Send the whole file as the body of a single PUT to upload.url, with Content-Type set to the same value as the contentType you declared when creating the job. Do not set a Content-Range header.
curl -X PUT 'https://example.com/storage/...' \
-H 'Content-Type: video/mp4' \
--upload-file product-launch.mp4
The upload URL is self-credentialed. A successful upload returns 200 OK from storage. Once that lands, processing starts automatically and the job moves to IN_PROGRESS on the next poll.
The upload URL expires at upload.expiresAt. If the URL expires before the upload completes, the upload will fail. In that case, create a new job (use a new Idempotency-Key) to get a fresh upload URL.
The base URL for the upload is different from the API base URL. This is expected — the signed URL is a direct link to our storage provider. Treat it as opaque.
Poll the job
GET /v1/soundtracking/jobs/{jobId} — see Get a soundtrack job in the API reference.
curl 'https://api.epidemicsound.com/v1/soundtracking/jobs/{jobId}' \
-H 'Authorization: Bearer your-api-key' \
-H 'x-partner-user-id: user-abc-123'
Poll no more than once every 5 seconds. Runs are long, and longer videos take longer. When the job reaches a terminal status (COMPLETED or FAILED), stop polling.
A 402 response means the workspace ran out of credits after the upload completed. The job status will also be FAILED with code INSUFFICIENT_CREDITS.
Download the output as soon as the job reaches COMPLETED and store it on your side. We don't guarantee how long the output file is kept. If result.output.url expires before you download the file, poll the job again for a new link.
Example — polling loop (Node.js)
async function waitForJob(apiKey, userId, jobId) {
const url = `https://api.epidemicsound.com/v1/soundtracking/jobs/${jobId}`
const headers = {
Authorization: `Bearer ${apiKey}`,
'x-partner-user-id': userId,
}
while (true) {
const res = await fetch(url, { headers })
if (!res.ok) throw new Error(`Poll failed: ${res.status}`)
const { status, result, error } = await res.json()
if (status === 'COMPLETED') return result
if (status === 'FAILED')
throw Object.assign(new Error(`Soundtrack job failed: ${error.code}`), {
code: error.code,
retriable: error.retriable,
})
await new Promise((r) => setTimeout(r, 5000))
}
}
Job lifecycle
status is the only field a client should branch on. It follows a linear lifecycle, ending in exactly one of two terminal states.
| Status | Terminal | Description |
|---|---|---|
AWAITING_USER_ACTION | No | Reserved. No soundtrack job emits this status yet. |
AWAITING_FILE_UPLOAD | No | The job exists and the upload URL is live. No bytes have arrived yet. |
IN_PROGRESS | No | The bytes arrived and processing is underway — queuing, video analysis, music selection and render. A client cannot act differently on any of these sub-steps. |
COMPLETED | Yes | The run finished successfully. result is present and carries the output file. |
FAILED | Yes | The run ended in failure. error is present and carries the cause. |
This enum is closed on purpose: these five values are exhaustive. A client that switches on status knows it has covered every case.
Outcomes that are neither success nor failure — a cancelled run, for example — surface as FAILED with a distinguishing error.code. There is no CANCELLED status.
The result
When status is COMPLETED, the response includes a result object.
{
"id": "9c3b1a2e-...",
"status": "COMPLETED",
"result": {
"output": {
"url": "https://example.com/storage/...",
"format": "MP4",
"expiresAt": "2026-09-27T18:00:00Z"
},
"media": [
{ "id": "abc123", "type": "MUSIC" }
]
},
...
}
result.output
The finished file in the format you requested at create time.
| Field | Description |
|---|---|
url | Signed URL to the file. Expires at expiresAt. |
format | The format: MP4, MP3 or WAV. |
expiresAt | When url stops working. |
result.media
Every library item that ended up in the output, in no particular order. Each entry has an id (the recording ID in the Epidemic Sound catalog) and a type (currently always MUSIC). Use these to look up tracks via the content API for attribution or to display credits.
Usage is tracked automatically for each completed job — you do not need to call the usage reporting endpoint for items in result.media.
Errors
There are two distinct error shapes.
Job errors — async failures
When a job itself fails, status is FAILED and the response includes an error object.
{
"status": "FAILED",
"error": {
"code": "VIDEO_TOO_LONG",
"retriable": false
}
}
| Field | Description |
|---|---|
code | Open string (not an enum). Branch on retriable for codes you do not recognise. |
retriable | true if submitting a new job with the same video is worth trying. false means the input must change first. |
There is no message field on this object — the underlying cause is written as internal prose for support and is not part of the public response. Build any user-facing messaging off code.
Known code values:
| Code | Retriable | Meaning |
|---|---|---|
UPLOAD_EXPIRED | true | The upload URL expired before the video arrived. Create a new job with a new Idempotency-Key to get a fresh URL. |
UNSUPPORTED | false | The file arrived but is not a video the service can decode. |
VIDEO_TOO_LONG | false | The video is longer than 4 minutes. Duration can only be measured after upload. |
NO_MUSIC_FOUND | false | The run completed but found nothing suitable for the timeline. A video with no soundtrack is not a success. |
INSUFFICIENT_CREDITS | false | The workspace could not cover the cost when the upload finished. Add credits and create a new job. |
RATE_LIMITED | true | No processing capacity was available in time. |
TIMEOUT | true | A processing step did not finish within its deadline. |
INTERNAL_ERROR | true | Anything else. |
code is an open string so the taxonomy can grow without a breaking change. For codes not listed here, treat retriable as the source of truth.
HTTP errors — synchronous failures
Failures during the create or poll requests return an application/problem+json body (RFC 9457). This shape differs from the one described in Error handling, which covers the v0 endpoints.
{
"type": "/problems/credits/insufficient-credits",
"title": "The workspace does not have the credits for this soundtrack."
}
When the request itself is invalid, the 400 response adds an errors array. pointer names the field at fault:
{
"type": "/problems/invalid-data",
"title": "The request is not valid.",
"errors": [
{
"pointer": "sizeBytes",
"detail": "int: value 999999999999 greater than 629145600"
}
]
}
| Field | Description |
|---|---|
type | Identifies the condition. Branch on this to handle a specific case; treat unknown values as generic errors. |
title | Human-readable summary for debugging. Build user-facing messages from the status and type. |
errors | Present on 400 validation failures only. |
A missing or invalid API key is rejected before the request reaches the API. That 401 response does not carry a problem document, so branch on the status code.
Every response from the API includes an X-Correlation-Id header. Include it when you contact Epidemic Sound about a failed request.
Idempotency
The create endpoint accepts an optional Idempotency-Key header. A repeat request carrying a key already used in the same workspace returns the existing job instead of provisioning a new one — safe to use when a network timeout leaves you unsure whether the first call landed.
curl -X POST 'https://api.epidemicsound.com/v1/soundtracking/jobs' \
-H 'Authorization: Bearer your-api-key' \
-H 'x-partner-user-id: user-abc-123' \
-H 'Idempotency-Key: my-unique-key-v1' \
-H 'Content-Type: application/json' \
-d '{ ... }'
The key identifies the request on its own — the body is not compared. If you need to retry with different parameters, use a different key, otherwise you will receive the earlier job back. Keys are 1–255 characters, caller-chosen, and opaque to the API.
Credits
Each job costs 2,500 AI credits from your workspace. Your allowance is set in your plan agreement. If your plan has monthly renewable credits, they renew on the anniversary of the date your account was created. Unused credits do not carry over. You can see your credit usage in the Developer Portal.
Creating a job checks that the balance covers the job, and returns 402 with /problems/credits/insufficient-credits if it does not. The same response is returned when the workspace has no access to AI credits.
The credits are taken when the video upload completes, not when the job is created. If you create two jobs with only enough credits for one, both pass the check and the second fails after its upload with the job error INSUFFICIENT_CREDITS.
Limits
| Limit | Value |
|---|---|
| Maximum video duration | 4 minutes |
| Maximum upload size | 600 MB (629,145,600 bytes) |
The 4-minute ceiling is enforced after upload — the API has no way to measure duration until the bytes arrive and the video is probed. A video longer than 4 minutes fails as VIDEO_TOO_LONG after the upload completes successfully. Plan for this in your UI.
The size limit is enforced at create time against the sizeBytes field. The upload is refused before any bytes move if sizeBytes exceeds 600 MB.
The two limits together imply an average bitrate ceiling of roughly 20 Mbps. 1080p footage typically fits within this; 4K or ProRes masters often do not.
Troubleshooting
Rate limits
Rate limits apply per workspace and are shared by all API keys and end users of your app. There are two windows: a daily quota that resets at midnight UTC, and a per-second burst limit. The values depend on your plan, and every response reports both:
RateLimit-Policy: "burst";q=521;w=1, "daily";q=9000000;w=86400
RateLimit: "burst";r=520;t=1, "daily";r=8999971;t=52183
q is the quota, w the window in seconds, r the requests remaining in the window, and t the seconds until it resets. A request over either limit returns 429 with a Retry-After header giving the seconds to wait.
These limits are counted separately from the v0 endpoints, so the limits in the general Troubleshooting guide do not apply here.
Transient errors
502, 503 and 504 are transient. Retry with exponential backoff, and when the response carries Retry-After, wait that many seconds instead. Retry the create call with the same Idempotency-Key so the retry returns the original job rather than creating a second one.