Guides
How to Validate a Video Before Sending It to an AI Music API
- Written by
- Sonilo Team
- Published

Check the account's current upload cap, read the real file size, probe the container for a video stream and finite duration, and reject anything outside the endpoint contract before you create a generation request.
Check the account's current upload cap, read the real file size, probe the container for a video stream and finite duration, and reject anything outside the endpoint contract before you create a generation request. File extensions and browser-supplied MIME types can be useful hints, but they are not enough.
By Sonilo Team · Facts verified August 14, 2026
Disclosure: Sonilo publishes this guide and provides the API used in the example. The `video-preflight-v1` code and tests are an application-side compatibility gate, not a security certification or a guarantee that every accepted file will generate successfully.
Validate the file before the generation POST
The current Sonilo Video to Music endpoint accepts exactly one source: an uploaded `video` or a `video_url`. This guide covers a local uploaded file. For remote objects, use the separate presigned source-URL lifetime gate.
A useful local acceptance sequence is:
- Authenticate your server and read the account's live upload limit.
- Measure the actual file in bytes.
- Run `ffprobe` with a bounded timeout and machine-readable JSON output.
- Require at least one video stream and a positive finite duration.
- Reject a duration above the endpoint ceiling.
- Store the acceptance record, then construct the multipart request.
The order matters. A filename ending in `.mp4` says nothing about whether a video stream is present. A `Content-Type: video/mp4` header is supplied by the client and can be wrong or spoofed. The OWASP File Upload Cheat Sheet recommends layered validation, including size limits, allowlists, type and signature checks, generated filenames, authorization, separated storage, and scanning where appropriate.
Read live account limits instead of copying an example
The authenticated Sonilo List Services endpoint returns enabled services and `max_upload_size_mb`. Its documentation shows 300 MB as an example, but the account response is the value your application should enforce.
The current Sonilo API reference documents a six-minute maximum for video-to-music. It maps an oversized upload to HTTP `413`, and an over-duration or unprobeable video to HTTP `422`. Local rejection gives the user a specific correction before a large upload and generation request begins.
| Gate | Local evidence | Reject when |
|---|---|---|
| Service | Account service list | Music service is unavailable |
| Size | File bytes and live cap | Bytes exceed the cap |
| Structure | Probe stream list | No video stream or probe error |
| Duration | Probe duration | Not finite, not positive, or over 360 seconds |
| Security | Upload policy | Authorization, scan, or isolation fails |
Do not turn observed codecs into an undocumented provider allowlist. Record them for support and diagnostics, but let the current endpoint contract remain authoritative.
A runnable Node.js preflight
Install FFmpeg so `ffprobe` is on the trusted server's executable path. Save the following lines as `video-preflight.mjs`. The example uses Node's file stats for bytes and execFile so the filename is passed as an argument without a shell by default.
- `import {execFile} from "node:child_process";`
- `import {stat} from "node:fs/promises";`
- `import {promisify} from "node:util";`
- `const run = promisify(execFile);`
- `const filePath = process.argv[2];`
- `const apiKey = process.env.SONILO_API_KEY;`
- `if (!filePath || !apiKey) throw new Error("Usage: SONILO_API_KEY=... node video-preflight.mjs /path/to/video");`
- `const servicesUrl = "https://api.sonilo.com" +`
- `"/v1/account/services";`
- `const limitsResponse = await fetch(servicesUrl, {`
- `headers: {Authorization: "Bearer " + apiKey},`
- `signal: AbortSignal.timeout(10000),`
- `});`
- `if (!limitsResponse.ok) throw new Error("Account services failed: " + limitsResponse.status);`
- `const limits = await limitsResponse.json();`
- `const enabled = limits.available_services ?? [];`
- `if (!enabled.includes("video_to_music")) {`
- `throw new Error("video_to_music is not enabled");`
- `}`
- `const file = await stat(filePath);`
- `if (!file.isFile() || file.size <= 0) throw new Error("Source must be a non-empty regular file");`
- `const maxBytes = limits.max_upload_size_mb * 1000000;`
- `if (!Number.isFinite(maxBytes) || maxBytes <= 0) throw new Error("Invalid live upload limit");`
- `if (file.size > maxBytes) throw new Error("File exceeds the live account upload limit");`
- `const fields = "format=duration,format_name:" +`
- `"stream=index,codec_type,codec_name";`
- `const args = [`
- `"-v", "error",`
- `"-show_entries", fields,`
- `"-of", "json",`
- `filePath,`
- `];`
- `const probeOptions = {timeout: 15000, maxBuffer: 1000000};`
- `const {stdout} = await run("ffprobe", args, probeOptions);`
- `const probe = JSON.parse(stdout);`
- `const streams = Array.isArray(probe.streams) ? probe.streams : [];`
- `const videoStreams = streams.filter((stream) => stream.codec_type === "video");`
- `if (videoStreams.length === 0) throw new Error("ffprobe found no video stream");`
- `const durationSeconds = Number(probe.format?.duration);`
- `if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) throw new Error("Invalid duration");`
- `if (durationSeconds > 360) throw new Error("Video exceeds the six-minute endpoint limit");`
- `console.log(JSON.stringify({accepted: true, fileSizeBytes: file.size, maxUploadSizeMb: limits.max_upload_size_mb, durationSeconds, videoStreamCount: videoStreams.length, videoCodecs: [...new Set(videoStreams.map((stream) => stream.codec_name).filter(Boolean))]}, null, 2));`
Run it with a server-side key and a local path:
- `SONILO_API_KEY=... node video-preflight.mjs ./edited-video.mp4`
The ffprobe documentation defines `-show_entries` and JSON output for selected format and stream fields. Keep the probe output narrow, bound execution time and buffered output, and run untrusted media in an isolated worker appropriate to your threat model.
What the acceptance record should retain
Store the decision with the object version or immutable file identity that will actually be uploaded. Otherwise a file can change after it passes validation.
| Field | Why keep it |
|---|---|
| Internal request ID and user or tenant ID | Joins authorization, validation, and submission without logging a credential |
| Immutable object key, version, or content hash | Binds the decision to the inspected bytes |
| File size and live account cap | Explains a size acceptance or rejection |
| Duration, container names, and video codecs | Supports diagnostics without inventing a provider allowlist |
| Video and audio stream counts | Distinguishes a real video stream from an audio-only or malformed input |
| Probe tool version and policy version | Makes later reproductions possible |
| Validation time and endpoint | Shows which contract governed the decision |
Do not store the bearer key, authorization header, or a signed source URL in this record. The Next.js server-boundary guide covers keeping the provider key out of browsers and logs.
Separate compatibility from upload security
`ffprobe` success means the tool parsed enough of the container to return metadata. It does not prove that the file is harmless, that all packets decode, that the provider supports every observed stream, or that the final generation will succeed.
For uploads from untrusted users, add the controls that fit your threat model: authenticate and authorize the uploader, isolate processing, use generated storage names, restrict extensions and detected types to what the business needs, scan where appropriate, prevent public execution, and apply storage and request quotas. OWASP explicitly describes defense in depth because no single upload check is sufficient.
This sample also reads metadata and then later uploads by path. In a high-assurance system, stage the file into immutable controlled storage or keep an open descriptor so the submitted bytes cannot be swapped between validation and use.
Preserve the API failure contract after preflight
A preflight reduces predictable rejections; it does not replace upstream error handling. Keep the provider's status and safe error code so your application can decide correctly.
- Treat `400`, `413`, and `422` as input corrections. Do not retry the same bytes unchanged.
- Treat `401` and `403` as credential or access problems. Stop until configuration changes.
- Treat `429` as capacity control. Respect `Retry-After` when present and stay within a bounded retry budget.
- Treat `502` or a network timeout during the generation POST as an uncertain submission outcome. Do not blindly submit another billable job.
Use the duplicate AI audio job guard before replaying an ambiguous POST. The current Sonilo API reference says the API sends no webhooks or callbacks: video-to-music streams NDJSON by default or returns a task ID in async mode, and other async products are read through the task endpoint. Use the bounded polling workflow after a recoverable task ID exists.
The sample does not retry `GET /account/services`; production code may use a short cached last-known-good limit only if the policy fails closed when stale. Never substitute a hardcoded example indefinitely.
The tested contract
The accompanying `video-preflight-v1` package separates the pure decision from filesystem, network, and process calls. Nine Node.js tests passed on August 14, 2026:
- Accept a valid probed video inside the live limits.
- Accept exact size and duration boundaries.
- Reject one byte over the live size cap.
- Reject metadata with no video stream.
- Reject zero or missing duration.
- Reject a duration above 360 seconds.
- Reject a missing live account cap.
- Keep an adversarial-looking filename as one `execFile` argument.
- Accept a silent video while recording zero audio streams.
These are deterministic unit tests using controlled probe JSON. `ffprobe` was not installed in the test runtime, so the evidence does not claim an end-to-end parse of a real media file. Run the same suite plus representative clean, damaged, oversized, over-duration, audio-only, and hostile fixtures inside your production media sandbox.
When not to use this exact gate
Do not use a local-file preflight for a remote `video_url`; validate source access, expiry, redirects, and network policy in a controlled ingestion workflow instead. Do not run the sample directly on an untrusted public web server with broad filesystem access. Do not assume six minutes or an example upload cap applies to a different product or provider.
If your application only handles trusted files that a human has already exported and reviewed, a smaller size-and-duration check may be enough. If strangers can upload arbitrary media, this sample is only the compatibility layer inside a larger security boundary.
Make validation the first lifecycle gate
Reject a file while the application can still explain the correction clearly. Then submit once, store the task identity, poll with a bounded schedule, and copy successful outputs to durable storage.
For the broader request and response flow, use the Sonilo Video-to-Music API guide. For exact fields and current limits, implement against the Sonilo API reference, OpenAPI 1.0.0, and your authenticated account response.


