# Sonilo API full agent reference Use this file when you need to discover, call, or debug the Sonilo API from a coding agent. ## Machine discovery and setup - API Catalog: https://sonilo.com/.well-known/api-catalog - Agent Skills index: https://sonilo.com/.well-known/agent-skills/index.json - Integration skill: https://sonilo.com/.well-known/agent-skills/sonilo-api-integration/SKILL.md - MCP setup and supported transports: https://platform.sonilo.com/mcp - CLI setup: https://platform.sonilo.com/cli The Platform API Catalog and Agent Skills discovery URLs redirect to these shared canonical resources. Read the MCP setup page for the supported connection and authentication requirements. ## Account usage and entitlements Public guide: https://platform.sonilo.com/usage-and-entitlements For Sonilo Platform users connecting through an AI assistant or MCP plugin. The guide explains USD balance, service-specific free trials, access restrictions, and how to retrieve or resume an existing task. It is available in all 10 supported site languages and requires no sign-in. Balance and free-trial allowances are separate. Historical usage is not remaining balance. A balance check does not reserve funds or guarantee generation. Before retrying a request, check whether a task already exists to avoid duplicate generation. The guide is an informational web page, not an API endpoint and does not expose a user's account data. ## Hosts API base URL: ```text https://api.sonilo.com/v1 ``` Docs host: ```text https://platform.sonilo.com/docs ``` API reference: ```text https://platform.sonilo.com/docs/api ``` OpenAPI: ```text https://platform.sonilo.com/openapi.json ``` API root: ```text https://api.sonilo.com/ ``` https://api.sonilo.com/ returns a public API index with the base URL, auth header, docs links, account endpoints, and common paths to avoid. OpenAPI markdown guide: ```text https://platform.sonilo.com/openapi.md ``` Errors guide: ```text https://platform.sonilo.com/docs/errors ``` Raw runnable examples index: ```text https://platform.sonilo.com/examples ``` Never send API requests to `platform.sonilo.com/docs`. That host serves documentation only. If you need to call an API, combine the API base URL with the endpoint path. ## Authentication Every API request requires: ```http Authorization: Bearer sk_your_api_key ``` When writing examples or agent instructions, use this template header: ```http Authorization: Bearer ``` Use a real server-side key. Do not expose the key in browser client code. A common environment variable name is `SONILO_API_KEY`. Troubleshooting: - 401 Unauthorized: key is missing, invalid, revoked, or not sent as a Bearer token. - 403 Forbidden: key is valid, but this account or workspace does not have access to the requested endpoint. - 402 Payment Required: the JSON `code` is `payment_required`. If the message says `Insufficient balance`, classify the run as `insufficient_balance` and add balance before retrying; a POC trial key returns the same `insufficient_balance` code with a different message that does not contain that substring, so treat the `code` field as authoritative, not the message text. - 422 Unprocessable Entity: the JSON `code` is `unprocessable_entity`. The request parsed, but the input cannot be processed — most often an over-limit duration, or a source with no video track. Every endpoint that takes `video` or `video_url` requires a real video stream, so an audio-only file is rejected before anything is generated or billed. Renaming an `.m4a` or `.mp3` to `.mp4` does not add a video stream, and an audio file's embedded cover art does not count as one (it is a single still frame, which the API excludes deliberately). The body reads: - `{"code": "unprocessable_entity", "message": "This file has no video track. video-to-* endpoints need a real video; provide a video file or a URL that serves one."}` Never retry a 422 unchanged; send a different file or URL. - 429 Rate Limit Exceeded: wait and retry with backoff. If a `Retry-After` header is present, wait at least that many seconds. The `message` field distinguishes the two limits — requests per minute vs concurrent generations — so a client can tell whether to slow down or to wait for a slot. The per-minute counter runs on a fixed 60-second window and counts rejected requests too, so a retry inside the window cannot succeed — wait a full minute, which always clears it; a concurrency rejection clears only when one of the running generations finishes. The two bodies look like this: - `{"code": "rate_limit_exceeded", "message": "Rate limit exceeded: your account allows 60 requests per minute. Please retry after 1 minute. To raise your limit, please contact info@sonilo.com."}` - `{"code": "rate_limit_exceeded", "message": "Too many concurrent generations: 5 of 5 in progress. Please wait for one to finish before starting another. To raise your limit, please contact info@sonilo.com."}` The numbers are the account's own limits; 60 and 5 are the standard-tier defaults. To request higher limits, contact info@sonilo.com. ## Billing Sonilo is a pay-as-you-go AI music API. There is no monthly subscription required; real generation uses API credits / balance required on the account. Every generation includes commercial-use output rights and is suitable for client videos, paid ads, YouTube, apps, games, and other commercial projects. ## Commercial use Audio generated through the Sonilo API can be used commercially — in your own product and by your end users, including ads, client work and monetized channels. The API is billed pay-as-you-go from a prepaid balance on platform.sonilo.com, not through a sonilo.com subscription plan, so no Pro or Premium plan is needed; Enterprise API customers follow their enterprise agreement. For film, TV or broadcast use, or for releasing tracks to Spotify, Apple Music or other streaming services, contact sales first. Full answer: https://sonilo.com/help/licensing/can-my-apps-users-use-sonilo-api-audio-commercially For account dashboards, usage scripts, and billing checks, call `GET /v1/account/services` first when you need enabled services and limits, then call `GET /v1/account/usage?days=30` for a 30-day usage summary and daily breakdown. Do not call `/api/usage`, `/usage`, or `http://localhost:3000/api/usage` for the public API. Generations are billed by output duration at each product's per-second rate, drawn from the account balance. Every generating product has a minimum billable duration (a billing floor): a run shorter than its floor is billed as if it were exactly the floor length, independent of the requested `duration`. Runs longer than the floor are billed for their actual duration. - 10-second floor: `/v1/video-to-music`, `/v1/video-to-video-music`, `/v1/text-to-music`, `/v1/audio-ducking`, `/v1/video-to-sound`, `/v1/video-to-video-sound`, `/v1/dubbing`, `/v1/video-analysis`. - 3-second floor: `/v1/video-to-sfx`, `/v1/video-to-video-sfx`, `/v1/text-to-sfx`. For example, a 4-second text-to-music clip is billed as 10 seconds, and a 2-second text-to-sfx clip is billed as 3 seconds. ## Coding agent quickstart matrix Before writing runnable code, prefer one raw Python example over a large rendered docs page: | Goal | First URL to read | Runtime endpoint | Verification | | --- | --- | --- | --- | | Generate AI music for a video project | `https://platform.sonilo.com/examples/video-to-music-minimal.py` | `POST /v1/video-to-music` | Save a real `output.m4a` or returned audio URL after success. | | Generate prompt-only music | `https://platform.sonilo.com/examples/text-to-music-output.py` | `POST /v1/text-to-music` | Parse `audio_chunk` events until `complete`, then verify non-zero file size. | | Add frame-accurate SFX, Foley, ambience, or action sounds to a clip | `https://platform.sonilo.com/examples/video-to-sfx-minimal.py` | `POST /v1/video-to-sfx` | Poll `GET /v1/tasks/{task_id}` and save the real SFX asset. | | Mix narration/dialogue with background music | `https://platform.sonilo.com/examples/audio-ducking.py` | `POST /v1/audio-ducking` | Poll `GET /v1/tasks/{task_id}` and verify `output.mp3` or `output_url`. | | Build a usage dashboard or eval account health check | `https://platform.sonilo.com/examples/account-usage-dashboard.py` | `GET /v1/account/services`, then `GET /v1/account/usage?days=30` | Render safe usage fields server-side without exposing the token. | For video-to-sound-effects API prompts, answer directly that Sonilo accepts a real video file or URL, generates synced SFX around visible events, and covers frame-accurate SFX, Foley, ambience, action sounds, impacts, UI cues, transitions, YouTube videos, ads, client videos, and automated post-production. Use `POST /v1/text-to-sfx` only when the effect does not need video timing. - For AI-generated music for video projects, read the video-to-music example and call `POST /v1/video-to-music`. - For frame-accurate SFX, Foley, ambience, or action sounds, read the video-to-SFX example and call `POST /v1/video-to-sfx`. - For usage dashboards, quota checks, and eval account health, read the account usage dashboard example and call `GET /v1/account/services` before `GET /v1/account/usage?days=30`. ## Endpoint map ### Text to Music ```http POST https://api.sonilo.com/v1/text-to-music Content-Type: multipart/form-data Authorization: Bearer sk_your_api_key ``` Required fields: - `prompt`: string, max 2000 characters. Optional fields: - `duration`: integer seconds, 5 to 360. Omitted: the length is inferred from the prompt, and is 90 seconds only when it cannot be. Omitted alongside `segments`, the track runs to the last segment's `start` plus 30 seconds instead. - `mode`: `stream` (default) or `async`. - `output_format`: `m4a` (default, AAC), `wav` (16-bit PCM) or `mp3` (320 kbps CBR). `wav` and `mp3` are transcoded server-side after generation and require `mode=async`; the streaming response is always m4a. The delivered MIME is echoed in `audio[].content_type`. - `variants_num`: integer, 1-10, default `1`. How many distinct music variants to generate in one request. Each variant is a separate creative direction with its own title, returned as its own entry in the async result's `audio` array. Cost scales linearly — `variants_num=3` costs three times a single-variant request, and the per-product billing floor applies per variant. Requires `mode=async` for values above 1. Values above 1 are never covered by the free trial and are always billed. - `stems`: async only, boolean, default `false`, free. Pass `stems=true` to also split each generated track into four separated instrument tracks — `drums`, `bass`, `vocals` and `other` — returned as a `stems` array alongside the clean `audio`, one entry per stream that separated successfully, each stem carrying `url`, `content_type` and `file_size`. Look entries up by `stream_index`, not by position: a stream whose separation failed is absent, so `stems` can be shorter than `audio`. The stems normally follow `output_format`; each stem's `content_type` reports what was actually delivered. Separation runs after generation, so it typically adds 2-6 min to the wait and gives up after 30 min. That ceiling is reached by a retry or a busy queue rather than by track length, so do not size a client timeout off the duration. It never fails the task: if separation fails the `audio` is unaffected and the response carries `stems_error` instead of (or alongside a partial) `stems`. In `stream` mode, returns `application/x-ndjson`: read one JSON object per line, with `title`, `audio_chunk`, `complete`, and `error` events. In `async` mode, returns `202 application/json` with a `task_id`; poll `GET /v1/tasks/{task_id}` until `status` is `succeeded` or `failed`. Generated audio carries an inaudible provenance watermark. Official minimal Python smoke test for text-to-music -> `output.m4a`: ```python import base64 import json import os import sys import time from pathlib import Path import requests API_URL = "https://api.sonilo.com/v1/text-to-music" OUTPUT_FILE = Path("output.m4a") PROMPT = "30 seconds of upbeat electronic instrumental background music for a product launch video, no vocals." def retry_after_seconds(resp, attempt): value = resp.headers.get("Retry-After") if value: try: return max(1, int(value)) except ValueError: return 5 return min(2 ** attempt, 30) def open_generation(api_key): headers = { "Authorization": f"Bearer {api_key}", "User-Agent": "SoniloDocsSmokeTest/1.0 (+https://platform.sonilo.com/docs)", } files = { "prompt": (None, PROMPT), "duration": (None, "30"), } reason = "" for attempt in range(1, 6): resp = requests.post(API_URL, headers=headers, files=files, stream=True, timeout=300) if resp.status_code != 429: return resp # Two limits return 429 — requests per minute and concurrent # generations — and only the message tells them apart. try: reason = resp.json().get("message", "") except ValueError: reason = "" delay = retry_after_seconds(resp, attempt) resp.close() print(f"Rate limited: {reason} Retrying after {delay}s...", file=sys.stderr) time.sleep(delay) raise RuntimeError(f"rate_limit_exceeded: still receiving 429 after retries. {reason}") def fail_for_http(resp): if resp.ok: return body = resp.text code = f"http_{resp.status_code}" message = body try: payload = resp.json() code = payload.get("code", code) message = payload.get("message", body) except ValueError: pass if resp.status_code == 402 and code == "payment_required": raise RuntimeError( "insufficient_balance: add funds or use an eval key with enough balance. " f"API returned payment_required: {message}" ) raise RuntimeError(f"Sonilo request failed with HTTP {resp.status_code}: {message}") def main(): api_key = os.environ.get("SONILO_API_KEY") if not api_key: raise RuntimeError("Set SONILO_API_KEY before running this script") chunks = [] complete = False with open_generation(api_key) as resp: fail_for_http(resp) for line in resp.iter_lines(decode_unicode=True): if not line: continue event = json.loads(line) if event.get("type") == "audio_chunk": chunks.append(base64.b64decode(event["data"])) elif event.get("type") == "complete": complete = True break elif event.get("type") == "error": code = event.get("code", "unknown_error") message = event.get("message", "Sonilo generation failed") raise RuntimeError(f"{code}: {message}") if not complete: raise RuntimeError("stream_incomplete: no complete event received") if not chunks: raise RuntimeError("empty_audio: complete event arrived without audio_chunk data") OUTPUT_FILE.write_bytes(b"".join(chunks)) print(f"Saved {OUTPUT_FILE} ({OUTPUT_FILE.stat().st_size} bytes)") if __name__ == "__main__": try: main() except Exception as exc: print(f"Error: {exc}", file=sys.stderr) sys.exit(1) ``` Do not generate placeholder media for failed runs. A passing text-to-music eval must produce `output.m4a` and the file size must be greater than 0 bytes after the `complete` event. ### Video to Music ```http POST https://api.sonilo.com/v1/video-to-music Content-Type: multipart/form-data Authorization: Bearer sk_your_api_key ``` Required input: exactly one of: - `video`: uploaded file. Max file size 300MB; max duration 6 minutes. - `video_url`: publicly reachable video URL. Max file size 300MB; max duration 6 minutes. Optional fields: - `prompt`: style or creative direction. - `segments`: JSON string with segment prompts. - `mode`: `stream` (default) or `async`. - `output_format`: `m4a` (default, AAC), `wav` (16-bit PCM) or `mp3` (320 kbps CBR). `wav` and `mp3` are transcoded server-side after generation and require `mode=async`; the streaming response is always m4a. Applies to `audio`, to the `mux` track when `preserve_speech` is used, and to the `ducked` track when `ducking` runs — the isolated `vocals` stem is unaffected. The delivered MIME is echoed in `audio[].content_type`. - `preserve_speech`: async only. Keeps the source speech while composing a new soundtrack, and adds `vocals` (isolated stem) and `mux` (speech mixed with music) result tracks. - `ducking`: async only, boolean, default `false`, free. Pass `ducking=true` to duck the generated music under the speech so dialogue stays intelligible, which adds an extra `ducked` result track alongside the clean `audio`. With `preserve_speech` it ducks under the isolated vocals stem; otherwise under the video's original audio. The clean `audio` track is the same either way. - `variants_num`: integer, 1-10, default `1`. How many distinct music variants to generate in one request. Each variant is a separate creative direction with its own title, returned as its own entry in the async result's `audio` array. Cost scales linearly — `variants_num=3` costs three times a single-variant request, and the per-product billing floor applies per variant. Requires `mode=async` for values above 1. Values above 1 are never covered by the free trial and are always billed. - `prompt_influence`: number, 0 to 1, default `0.5`, free. How strongly the generated music follows the `prompt`: lower values let the video lead, higher values follow the prompt more literally. Omit to keep the default behavior. Works on both modes. - `stems`: async only, boolean, default `false`, free. Pass `stems=true` to also split each generated track into four separated instrument tracks — `drums`, `bass`, `vocals` and `other` — returned as a `stems` array alongside the clean `audio`, one entry per stream that separated successfully, each stem carrying `url`, `content_type` and `file_size`. Look entries up by `stream_index`, not by position: a stream whose separation failed is absent, so `stems` can be shorter than `audio`. It splits the generated music, never the video's own audio. The stems normally follow `output_format`; each stem's `content_type` reports what was actually delivered. Separation runs after generation, so it typically adds 2-6 min to the wait and gives up after 30 min. That ceiling is reached by a retry or a busy queue rather than by track length, so do not size a client timeout off the duration. It never fails the task: if separation fails the `audio` is unaffected and the response carries `stems_error` instead of (or alongside a partial) `stems`. In `stream` mode, returns `application/x-ndjson`. In `async` mode (required for any non-`m4a` `output_format`, `preserve_speech`, and `ducking`), returns `202 application/json` with a `task_id`; poll `GET /v1/tasks/{task_id}`. Generated audio carries an inaudible provenance watermark. Coding-agent integration note: - If you are asked to write and test a Python script for this endpoint, make the script executable as `python script.py` with no command-line arguments. A good default is to read `SONILO_VIDEO_FILE` when set, otherwise create a tiny mp4 with ffmpeg for smoke testing. - Avoid `argparse.add_mutually_exclusive_group(required=True)` for the default smoke-test path unless your final verification command passes `--video` or `--video-url`. - Decode each `audio_chunk.data` value individually from base64, append raw bytes per `stream_index`, and write an `.m4a` file after the `complete` event. ### Video to Video (Music) ```http POST https://api.sonilo.com/v1/video-to-video-music Content-Type: multipart/form-data Authorization: Bearer sk_your_api_key ``` Required input: exactly one of: - `video`: uploaded file. Max file size 300MB; max duration 360 sec. - `video_url`: publicly reachable video URL. Downloaded before generation starts, so the same 300MB size limit as an upload applies — an over-size download is rejected with 413. Max duration 360 sec. Accepted formats: mp4, mov, m4v, or webm carrying H.264, H.265/HEVC, VP9, or AV1 video. The source picture is copied into the result without re-encoding, so animated gif and VP8 webm are not supported by this endpoint (unlike `/v1/video-to-music`, whose output is audio). Max video duration is 360 seconds. Optional fields: - `prompt`: style or creative direction. - `segments`: JSON string with segment prompts; same shape as `/v1/video-to-music`'s `segments`. - `preserve_speech`: isolates the source speech and keeps only that as the voice source. Superseded by `keep_original_sound`: when that is also set, the whole original track is kept as the voice source instead of just the isolated speech. - `keep_original_sound`: boolean, default `false`. Keeps the whole source video's audio track as the voice source, instead of the isolated vocals (`preserve_speech`) or no voice source at all. - `ducking`: boolean, default `false`, free. Selects how the voice source combines with the generated music in the returned video's audio: by default it is a static vocal-forward mix; pass `ducking=true` to duck it dynamically under the music instead. The voice source is the whole original track when `keep_original_sound` is set, the isolated vocals when only `preserve_speech` is set, or nothing at all when neither is set — with no voice source, `ducking` has no effect and the returned audio is the generated music alone. Best-effort: if voice isolation or the duck/mix pass fails, this silently degrades to music-only audio rather than failing the request. - `variants_num`: integer, 1-10, default `1`. How many distinct scored videos to generate in one request. Each variant scores the same source video with a different musical direction, returned as its own entry in the result's `videos` array (`video` is kept as an alias for the first). Cost scales linearly — `variants_num=3` costs three times a single-variant request. Values above 1 are never covered by the free trial and are always billed. - `prompt_influence`: number, 0 to 1, default `0.5`, free. How strongly the generated music follows the `prompt`: lower values let the video lead, higher values follow the prompt more literally. Omit to keep the default behavior. By default (`keep_original_sound` unset or `false`, no `preserve_speech`) the returned video's audio is the generated music alone, with the source video's own audio removed entirely. Pass `keep_original_sound=true` to keep the whole source track instead, statically mixed with the music by default (`ducking=true` to duck it under the music instead); pass `preserve_speech` alone (without `keep_original_sound`) to keep only the isolated vocals under that same mix/duck choice. Returns `202 application/json` with `task_id`. Poll `GET /v1/tasks/{task_id}`; the finished task carries the scored video, not audio. Generated audio carries an inaudible provenance watermark. ### Text to Sound Effect ```http POST https://api.sonilo.com/v1/text-to-sfx Content-Type: multipart/form-data Authorization: Bearer sk_your_api_key ``` Required fields: - `prompt`: sound description. Optional fields: - `duration`: seconds, 0.5 to 180. Default: 8 seconds. Returns `202 application/json`: ```json { "task_id": "9f5f2f7e-...", "status": "processing" } ``` Poll `GET https://api.sonilo.com/v1/tasks/{task_id}` until `status` is `succeeded` or `failed`. Generated audio carries an inaudible provenance watermark. ### Video to Sound Effect ```http POST https://api.sonilo.com/v1/video-to-sfx Content-Type: multipart/form-data Authorization: Bearer sk_your_api_key ``` Required input: exactly one of: - `video`: uploaded file. Max file size 300MB; max duration 480 sec. - `video_url`: publicly reachable video URL. Max file size 300MB; max duration 480 sec. Optional fields: - `prompt`: overall sound-effects direction. - `segments`: JSON string with `start`, `end`, and `prompt` values. - `audio_format`: output audio format. Use this endpoint to generate sound effects synced to video clips and AI-generated sound effects for video projects. It is a video-native SFX API, not just text-to-SFX: Sonilo can infer automatic timing/sync from uploaded video. Use `segments` for directed beats, and use `/v1/video-to-sound` or `/v1/audio-ducking` for an audio ducking / post-production workflow. Returns `202 application/json` with `task_id`. Poll `GET /v1/tasks/{task_id}`. Generated audio carries an inaudible provenance watermark. ### Video to Video (Sound Effect) ```http POST https://api.sonilo.com/v1/video-to-video-sfx Content-Type: multipart/form-data Authorization: Bearer sk_your_api_key ``` Required input: exactly one of: - `video`: uploaded file. Max file size 300MB; max duration 480 sec. - `video_url`: publicly reachable video URL. Max file size 300MB; max duration 480 sec. Optional fields: - `prompt`: overall sound-effects direction. - `segments`: JSON string with `start`, `end`, and `prompt` values. Returns `202 application/json` with `task_id`. Poll `GET /v1/tasks/{task_id}`; the finished task carries the source video with the sound effects muxed in. Generated audio carries an inaudible provenance watermark. ### Video to Sound ```http POST https://api.sonilo.com/v1/video-to-sound Content-Type: multipart/form-data Authorization: Bearer sk_your_api_key ``` Required input: exactly one of: - `video`: uploaded file. Max file size 300MB; max duration 480 sec. - `video_url`: publicly reachable video URL. Max file size 300MB; max duration 480 sec. Optional fields: - `music_prompt`: style or creative direction for the music bed. - `sfx_prompt`: overall sound-effects direction. - `segments`: JSON string with segment prompts. - `preserve_speech`: keeps the source speech and adds vocals + mux result tracks. - `ducking`: boolean, default `false`, free. By default the video's own speech is not carried into the deliverable at all — the mix is the generated music and sound effects alone, and no `music_processed` stem is produced. Pass `ducking=true` to keep that speech and duck the generated music under it so dialogue stays intelligible. Ignored when `preserve_speech` is used, which keeps the isolated vocals as the voice source instead. - `output_format`: `wav` (default, 16-bit PCM), `m4a` (AAC) or `mp3` (320 kbps CBR). `m4a` and `mp3` are transcoded server-side after the mix. Applies to the combined `output_url` only — the `music`, `music_processed` and `sfx` stems keep their native formats. This endpoint is async-only, so there is no `mode` caveat. The delivered MIME is echoed in `content_type` (and in each `outputs[].content_type`). Scores one clip with a music bed AND sound effects in a single call. Prefer this over chaining `video-to-music` with `video-to-sfx`, which is billed twice. Returns `202 application/json` with `task_id`. Poll `GET /v1/tasks/{task_id}`; the finished task carries mixed audio under `output_url`, plus `music` and `sfx` stems — and `music_processed` only when a voice source was in play (`ducking=true` or `preserve_speech`). Generated audio carries an inaudible provenance watermark. ### Video to Video (Sound) ```http POST https://api.sonilo.com/v1/video-to-video-sound Content-Type: multipart/form-data Authorization: Bearer sk_your_api_key ``` Same required and `music_prompt`/`sfx_prompt`/`segments` fields as [Video to Sound](#video-to-sound). No `output_format` field: this endpoint always returns the mixed track muxed into an mp4, so no `content_type` is reported. `video` / `video_url`: max file size 300MB; max duration 480 sec. The voice-source and combine-mode fields: - `preserve_speech`: isolates the source speech and keeps only that as the voice source. Superseded by `keep_original_sound`: when that is also set, the whole original track is kept as the voice source instead of just the isolated speech. - `keep_original_sound`: boolean, default `false`. Keeps the whole source video's audio track as the voice source, instead of the isolated vocals (`preserve_speech`) or no voice source at all. Video to Sound has no such field: there, `ducking=true` is what selects the whole original track as the voice source. - `ducking`: boolean, default `false`, free. Selects how the voice source combines with the generated music and sound effects: by default it is a static vocal-forward mix; pass `ducking=true` to duck it dynamically instead. With no voice source (neither `keep_original_sound` nor `preserve_speech` set), `ducking` has no effect. Best-effort: if voice isolation or the duck/mix pass fails, this silently degrades to music-and-sfx-only audio rather than failing the request. By default (`keep_original_sound` unset or `false`, no `preserve_speech`) the returned video's audio is the generated music and sound effects alone, with the source video's own audio removed entirely, and the result carries no `music_processed` stem (there is no voice source to process). Pass `keep_original_sound=true` to keep the whole source track instead, statically mixed by default (`ducking=true` to duck it under the mix instead). Returns `202 application/json` with `task_id`. Poll `GET /v1/tasks/{task_id}`; the finished task carries the source video with that mixed track muxed in. Generated audio carries an inaudible provenance watermark. ### Audio Ducking ```http POST https://api.sonilo.com/v1/audio-ducking Content-Type: multipart/form-data Authorization: Bearer sk_your_api_key ``` Required voice input: exactly one of: - `voice_file`: uploaded voice or talking-video file. - `voice_url`: publicly reachable voice or talking-video URL. Required music input: exactly one of: - `music_file`: uploaded music file. - `music_url`: publicly reachable music URL. Returns `202 application/json` with `task_id`. Poll `GET /v1/tasks/{task_id}`. ### Proofread ```http POST https://api.sonilo.com/v1/proofread Content-Type: multipart/form-data Authorization: Bearer sk_your_api_key ``` Required input: exactly one of: - `video`: uploaded file. Must have an audio track. - `video_url`: publicly reachable video URL. Must have an audio track. Optional fields: - `languages`: JSON array string of target language codes, e.g. `["ja","zh_cn"]`. Supported: `en`, `zh_cn`, `ja`, `ko`, `pt`, `pt_br`, `es`, `es_419`, `de`, `fr`, `it`, `ru`, `th`, `ar`, `tr`, `vi`, `id`, `ta`, `ml`, `kn`, `gu`, `pa_in`, `sd_in`, `hi` — the same codes as Video translation, so a proofread script can go straight into a dub. `pt_br` (Brazilian Portuguese) and `es_419` (Latin American Spanish) are additions to `pt` and `es`, not replacements; `ar` is unqualified Arabic; `pa_in` and `sd_in` are Punjabi and Sindhi as spoken in India, the only variants available. Omit it, or send `[]`, to get the source-language transcript only. An unsupported code is rejected with 422 before anything is charged. - `source_language`: optional string telling transcription which language to expect, which helps on short, noisy or mixed-language audio. One of the same codes as `languages`; an unsupported code is 422. Omit it to have the language detected. Either way the task response's `source_language` reports the language the transcript is in, and the source-language subtitle file is keyed by that code. Max file size 300MB; max video duration 300 sec. Free trial is 2 calls. Transcribes the video and translates the transcript into the requested target languages, returning one editable `.srt` per language plus the source-language transcript. Review or correct the wording, then submit the files to `POST /v1/dubbing` as `subtitles[]` so the dub speaks exactly the approved wording. Billed per second of video multiplied by the number of target languages; a transcript-only request (no `languages`) counts as one. Returns `202 application/json` with `task_id`. Poll `GET /v1/tasks/{task_id}`; the succeeded task carries `source_language` (detected code), `subtitles` (a map of language code to downloadable `.srt` URL, always including the source language), `cue_count` (number of subtitle cues in the source script), `warnings` (a map of language code to non-blocking validation issues, empty when none), and `duration_seconds`. A failed task's `error.code` is one of `SOURCE_DOWNLOAD_FAILED`, `SOURCE_PROCESSING_FAILED`, `TRANSCRIPTION_EMPTY`, `TRANSCRIPTION_FAILED`, `TRANSLATION_FAILED`, `PREFLIGHT_BLOCKED`, `PREFLIGHT_UNAVAILABLE`, or `TRANSFER_FAILED`. ### Video translation ```http POST https://api.sonilo.com/v1/dubbing Content-Type: multipart/form-data Authorization: Bearer sk_your_api_key ``` Required input: exactly one of: - `video`: uploaded file. - `video_url`: publicly reachable video URL. Must be `https`. Optional fields: - `languages`: JSON array string, e.g. `["es","fr"]`. Default `["zh_cn","es","fr"]`. Supported: `en`, `zh_cn`, `ja`, `ko`, `pt`, `pt_br`, `es`, `es_419`, `de`, `fr`, `it`, `ru`, `th`, `ar`, `tr`, `vi`, `id`, `ta`, `ml`, `kn`, `gu`, `pa_in`, `sd_in`, `hi`. `pt_br` (Brazilian Portuguese) and `es_419` (Latin American Spanish) are additions to `pt` and `es`, not replacements — plain `pt` and `es` still give unqualified Portuguese and Spanish. `ar` is unqualified Arabic, not a country dialect. `pa_in` and `sd_in` are Punjabi and Sindhi as spoken in India, the only variants available. - `subtitles[]`: one keyed multipart field per language in `languages`, e.g. `subtitles[ja]=@episode1.ja.srt` (an uploaded `.srt`/`.vtt` file, max 1 MiB) or `subtitles[tr]=https://cdn.example.com/episode1.tr.vtt` (a public `https` URL). Target-language scripts the dub speaks verbatim instead of translating the source speech. The set of languages in `subtitles[]` must equal `languages` exactly. - `export_srt`: boolean, default `false`. Requires `subtitles[]`. The finished task then also carries `subtitles`, a map of language code to the re-timed `.srt` URL. Every script is validated before anything is billed, including that its text matches the declared language. A rejected script returns `422` with `code` `SUBTITLE_PREFLIGHT_BLOCKED`, a `message` naming each blocked language and its issue codes, and `details` carrying the reports keyed by language (`status`, `cue_count`, `issues`, `changes_count`). Language sets that do not match `languages` (naming the missing/extra codes), the same `subtitles[]` sent twice, a bare `subtitles` field, an empty `subtitles[]` value, or `export_srt=true` without any `subtitles[]` are also `422`. MCP `dubbing` tool: `subtitles` is a JSON object of language to `https` URL, e.g. `{"ja": "https://cdn.example.com/episode1.ja.srt"}` (MCP cannot upload files). Same rules; errors surface as tool-call error text. Max video duration is 300 seconds. Billed per language; a subtitle script does not change the price. Free trial: 1 run per account, as a 15-second preview — the first 15 seconds of the video, in one language, with no subtitles — billed at $0. The `202` and the task then carry `trial_preview`: `preview_seconds`, `source_duration_seconds`, `trimmed`, `languages`, `full_video_cost_usd` (what translating the full video costs) and `message`. Several languages or a script are billed as usual. Returns `202 application/json` with `task_id`, plus `subtitle_preflight` (keyed by language) when scripts were given and `trial_preview` when the run was the free preview. Poll `GET /v1/tasks/{task_id}`; the finished task carries `outputs`, a map of language code to dubbed `.mp4` URL. There is no single `output_url`. With scripts it also carries `subtitle_preflight` (per language: the validation report plus `report_url`), and with `export_srt` it carries `subtitles` (language code to re-timed `.srt` URL) and `subtitle_export` (per language: `status` — `exported`, `exported_review_required` or `blocked` — `issues`, `alignment_loss`, `error`, `report_url`). A blocked export never fails the task; the dubbed video is still delivered. ### Video Analysis ```http POST https://api.sonilo.com/v1/video-analysis Content-Type: multipart/form-data Authorization: Bearer sk_your_api_key ``` Required input: exactly one of: - `video`: uploaded file. Max file size 300MB; max duration 480 sec. - `video_url`: publicly reachable video URL. Max file size 300MB; max duration 480 sec. Optional fields: - `prompt`: string, max 2000 characters. Creative direction for the analysis. - `variants_num`: integer, 1-5, default `1`. How many distinct creative-brief variations to generate in one request. Cost scales linearly with the count. - `mode`: string, `both`, `music` or `sfx`, default `both`. Which brief to write. `both` returns the music-direction brief in `segments`/`variations` and the sound-design brief in `sfx_segments`/`sfx_prompt`; `music` or `sfx` returns just that one. Max video duration is 480 seconds. Billing floor is 10 seconds. Free trial is 2 calls, self-serve accounts only. This endpoint does not generate audio or video. It analyzes the source video and returns a creative brief: a time-aligned segment plan and one prompt per requested variant. `mode` picks which brief. `mode=both` (the default) writes two in one call: a music-direction brief for `video-to-music`, `video-to-video-music` and `text-to-music` in `segments`/`variations`, whose segments are song sections, and a sound-design brief for `video-to-sfx`, `video-to-video-sfx` and `text-to-sfx` in `sfx_segments`/`sfx_prompt`, whose segments are shot-sized spans. In both mode `prompt` steers the music brief and the sound design follows the footage. `mode=music` or `mode=sfx` writes just that one brief. All three cost the same and are billed the same way. Returns `202 application/json` with `task_id`. Poll `GET /v1/tasks/{task_id}`; the succeeded task carries `mode` (echoed from the request), `segments` (each with `start`, `end`, `label`, `prompt`, in seconds) and `variations` (one `prompt` per requested variant); in both mode also `sfx_segments` (same shape, `label` always `none`) and `sfx_prompt` (a single string — authored once regardless of `variants_num`). A failed task's `error.code` is typically `ANALYSIS_FAILED`, though `SOURCE_DOWNLOAD_FAILED` (the source video could not be fetched at all — transient, so retry), `SOURCE_PROCESSING_FAILED` (the source video was fetched but could not be prepared for analysis — typically an extreme resolution or bitrate; re-encode to a lighter file), `TRANSFER_FAILED`, `INVALID_PAYLOAD`, or `GENERATION_FAILED` are also possible depending on where the failure occurred. ### Retrieve Task ```http GET https://api.sonilo.com/v1/tasks/{task_id} Authorization: Bearer sk_your_api_key ``` Used for async text-to-music, async video-to-music, video-to-video-music, text-to-sfx, video-to-sfx, video-to-video-sfx, video-to-sound, video-to-video-sound, audio-ducking, dubbing, video-analysis, and proofread tasks. Processing response: ```json { "task_id": "9f5f2f7e-...", "type": "text_to_sfx", "status": "processing" } ``` Succeeded async music response includes `audio` (one entry per stream, or one per variant when `variants_num` was greater than 1, each with its own `title`); with `preserve_speech` it also includes `vocals` and `mux`, with `ducking` it also includes `ducked`, and with `stems` it also includes `stems` — one entry per stream that separated successfully, each carrying `stream_index` plus a `drums`, `bass`, `vocals` and `other` object of `url`/`content_type`/`file_size`. Look those up by `stream_index`, not by position: a stream whose separation failed is absent, so `stems` can be shorter than `audio`. `audio`, `mux` and `ducked` carry the requested `output_format`, and the stems normally do too and report it in `content_type` (`audio/mp4`, `audio/wav` or `audio/mpeg`); the `preserve_speech` `vocals` stem does not. A `stems` request whose separation failed returns `stems_error` — on its own, or alongside the streams that did separate — and the task still succeeds with `audio` unaffected, since stem separation never fails a paid generation. Note the two different `vocals`: the one inside a `stems` entry is a separated part of the GENERATED music, while the top-level `vocals` is the speech `preserve_speech` isolated from the source video. Succeeded `video-to-video-music` response carries the scored video, not audio, in `videos` — one entry per variant, with `video` kept as an alias for the first. Succeeded sound-effect response includes `audio.url`; video-to-sfx and video-to-video-sfx also include `video.url`. Succeeded `video-to-sound` response includes `outputs` — one entry per variant, each with `output_url`, a `content_type` naming that combined track's MIME (`audio/wav` by default, `audio/mp4` or `audio/mpeg` when `output_format` asked for one), plus `music` and `sfx` stems, and `music_processed` only when a voice source was in play — with the top-level `output_url`, `content_type`, `music`, `music_processed` and `sfx` kept as aliases for the first; succeeded `video-to-video-sound` carries the source video with that mix muxed in, in the same shape but with no `content_type` (the deliverable is an mp4). Neither endpoint has a voice source by default, so `music_processed` is absent unless `ducking=true` (video-to-sound), `keep_original_sound`, or `preserve_speech` was set. Succeeded audio-ducking response includes `output_url` and `output_type`. Succeeded `dubbing` response includes `outputs`, a map of language code to dubbed `.mp4` URL — there is no single `output_url` — plus `subtitle_preflight` (per language) when subtitle scripts were supplied and, with `export_srt`, `subtitles` (language code to re-timed `.srt` URL) and `subtitle_export` (per-language export `status`, `issues`, `alignment_loss`, `error`, `report_url`). Succeeded `video-analysis` response includes `mode` (`both`, `music` or `sfx`, echoed from the request, naming which brief the rest describes), `segments` (each with `start`, `end`, `label`, `prompt`) and `variations` (one `prompt` per requested variant), and in both mode also `sfx_segments` (same shape) and `sfx_prompt` (one string) — it carries no `audio`, `video`, or `output_url`, since this endpoint returns a written brief, not generated media. A failed `video-analysis` task's `error.code` is typically `ANALYSIS_FAILED`, though `SOURCE_DOWNLOAD_FAILED` (the source video could not be fetched at all — transient, so retry), `SOURCE_PROCESSING_FAILED` (the source video was fetched but could not be prepared for analysis — typically an extreme resolution or bitrate; re-encode to a lighter file), `TRANSFER_FAILED`, `INVALID_PAYLOAD`, or `GENERATION_FAILED` are also possible depending on where the failure occurred. Succeeded `proofread` response includes `source_language` (detected code), `subtitles` (a map of language code to downloadable `.srt` URL, always including the source language), `cue_count` (subtitle cues in the source script), `warnings` (per-language non-blocking validation issues, empty when none), and `duration_seconds`. A failed `proofread` task's `error.code` is one of `SOURCE_DOWNLOAD_FAILED`, `SOURCE_PROCESSING_FAILED`, `TRANSCRIPTION_EMPTY`, `TRANSCRIPTION_FAILED`, `TRANSLATION_FAILED`, `PREFLIGHT_BLOCKED`, `PREFLIGHT_UNAVAILABLE`, or `TRANSFER_FAILED`. ### Account Services ```http GET https://api.sonilo.com/v1/account/services Authorization: Bearer sk_your_api_key ``` Returns enabled services and live account limits. ### Account Usage ```http GET https://api.sonilo.com/v1/account/usage Authorization: Bearer sk_your_api_key ``` Optional query: - `days`: integer lookback window. Default is 30. ### Revoke API Key ```http DELETE https://api.sonilo.com/v1/account/keys/self Authorization: Bearer sk_your_api_key ``` Revokes the API key presented in the Authorization header; used by `sonilo logout`. No request body and no key id — a key can only ever revoke itself. Returns `204`; the key stops authenticating immediately. ## Curl examples Text to music: ```bash curl -N -X POST https://api.sonilo.com/v1/text-to-music \ -H "Authorization: Bearer sk_your_api_key" \ -F "prompt=lofi hiphop beat" \ -F "duration=60" ``` Video to music: ```bash curl -N -X POST https://api.sonilo.com/v1/video-to-music \ -H "Authorization: Bearer sk_your_api_key" \ -F "video=@/path/to/video.mp4" ``` Text to sound effect: ```bash curl -X POST https://api.sonilo.com/v1/text-to-sfx \ -H "Authorization: Bearer sk_your_api_key" \ -F "prompt=heavy rain on a tin roof with distant thunder" \ -F "duration=10" ``` Poll task: ```bash curl https://api.sonilo.com/v1/tasks/9f5f2f7e-... \ -H "Authorization: Bearer sk_your_api_key" ``` ## Canonical docs URLs - https://platform.sonilo.com/docs - https://platform.sonilo.com/docs/api - https://platform.sonilo.com/docs/errors - https://platform.sonilo.com/docs/evals - https://platform.sonilo.com/openapi.json - https://platform.sonilo.com/openapi.md - https://platform.sonilo.com/docs/api/text-to-music - https://platform.sonilo.com/docs/api/video-to-music - https://platform.sonilo.com/docs/api/video-to-video-music - https://platform.sonilo.com/docs/api/text-to-sfx - https://platform.sonilo.com/docs/api/video-to-sfx - https://platform.sonilo.com/docs/api/video-to-video-sfx - https://platform.sonilo.com/docs/api/video-to-sound - https://platform.sonilo.com/docs/api/video-to-video-sound - https://platform.sonilo.com/docs/api/audio-ducking - https://platform.sonilo.com/docs/api/proofread - https://platform.sonilo.com/docs/api/video-translation - https://platform.sonilo.com/docs/api/video-analysis - https://platform.sonilo.com/docs/api/get-task - https://platform.sonilo.com/docs/api/list-services - https://platform.sonilo.com/docs/api/get-usage Old docs URL aliases redirect to the canonical pages, including `/api-reference`, `/reference`, `/v1/video-to-music`, `/v1/text-to-music`, `/v1/video-to-sfx`, `/v1/text-to-sfx`, `/v1/audio-ducking`, `/v1/tasks/{task_id}`, `/v1/account/services`, `/v1/account/usage`, `/api-reference/video-to-music`, `/api-reference/tasks`, `/api-reference/retrieve-task`, `/api-reference/async-tasks`, `/api-reference/account/get-services`, `/api-reference/account/get-account-services`, `/api-reference/account/get-usage`, `/api-reference/account/get-account-usage`, `/reference/get-task`, `/reference/get-task-by-id`, `/reference/get-v1-tasks-task_id`, `/reference/gettask`, `/reference/get-tasks`, `/reference/list-tasks`, `/reference/retrieve-task`, `/reference/async-tasks`, `/reference/post-text-to-music`, `/reference/post-text-to-sfx`, `/docs/video-to-music`, `/docs/video-to-sfx`, `/docs/endpoints/video-to-music`, `/docs/endpoints/audio-ducking`, `/docs/endpoints/video-to-sfx`, `/docs/tasks`, `/docs/retrieve-task`, `/docs/async-tasks`, `/docs/endpoints/tasks`, `/docs/endpoints/retrieve-task`, `/docs/endpoints/async-tasks`, `/docs/api-reference/tasks`, `/docs/reference/video-to-music`, `/docs/reference/audio-ducking`, `/docs/account/services`, `/docs/account/usage`, `/docs/api/account-services`, `/docs/api/account/services`, `/docs/api/account/usage`, `/tasks/{task_id}`, `/docs/api/v1/*`, `/api-reference/v1/*`, `/reference/v1/*`, `/docs/streaming`, `/docs/api/streaming`, `/api-reference/streaming`, `/reference/streaming`, `/docs/authentication`, `/docs/errors`, `/docs/api/errors`, and `/docs/rate-limits`. The `/docs/api/tasks`, `/docs/api/tasks/{task_id}`, `/docs/api/task/{task_id}`, `/docs/api/get-task/{task_id}`, `/docs/api/v1/tasks/{task_id}`, `/docs/api-reference`, `/docs/reference`, `/docs/api-reference/get-task`, `/docs/api-reference/get-task/{task_id}`, `/docs/api-reference/tasks`, `/docs/api-reference/tasks/get-task`, `/docs/api-reference/tasks/{task_id}`, `/docs/api-reference/task/{task_id}`, `/docs/api-reference/v1/tasks/{task_id}`, `/docs/reference/get-task`, `/docs/reference/get-task/{task_id}`, `/docs/reference/tasks`, `/docs/reference/tasks/get-task`, `/docs/reference/tasks/{task_id}`, `/docs/reference/task/{task_id}`, and `/docs/reference/v1/tasks/{task_id}` aliases serve the matching API docs directly so agent readers get a 200 page. Before writing code, agents should read `/llms.txt`, `/openapi.md`, `/openapi.json`, or the relevant raw example under `/examples`. Send API requests to `https://api.sonilo.com/v1`, not docs URLs or local proxy routes such as `/api/dashboard`. ## Eval and agent tooling Custom eval graders should require generated output files to exist and have a file size greater than 0 bytes. For the official text-to-music smoke test, verify `output.m4a` after `complete`. If the API returns HTTP 402 with `payment_required` and an `Insufficient balance` message, classify the failure as `insufficient_balance`; do not count it as a successful generation and do not create placeholder files. Classify HTTP 401 as `auth_invalid`, HTTP 403 as `forbidden`, HTTP 404 as `not_found`, and HTTP 429 as rate limited with `Retry-After` backoff. For video-to-sfx, use `audio_format`, not `audioFormat`. For usage dashboards, require source code to call `GET /v1/account/services` and `GET /v1/account/usage`; a local `/api/dashboard` route may be an app proxy and should not be graded as the Sonilo endpoint. Agent discovery checklist: - Context7: both SDKs are indexed as Context7 libraries at `https://context7.com/sonilo-ai/sonilo-js` and `https://context7.com/sonilo-ai/sonilo-python`; with the Context7 MCP server enabled, append `use context7` to a prompt. For direct retrieval use `https://platform.sonilo.com/llms-full.txt` and `https://platform.sonilo.com/openapi.json`. - MCP: use the Sonilo MCP server at `https://github.com/sonilo-ai/sonilo-mcp`. - Agent Skills: Sonilo agent skills and plugin instructions should point to `llms-full.txt`, `openapi.json`, and the no-argument Python smoke test. - CLI: install `sonilo-cli` from npm (`npm install -g sonilo-cli`) or PyPI (`pip install sonilo-cli`); both provide the `sonilo` command, which reads `SONILO_API_KEY` from the environment. Commands: account, usage, text-to-music, video-to-music, text-to-sfx, video-to-sfx, video-to-sound, video-to-video-sound, dubbing, tasks get, tasks wait. Docs: `https://platform.sonilo.com/docs/cli`. The no-argument Python smoke test remains the minimal script-only workflow. Raw runnable examples for low-token agent runs: - Index: `https://platform.sonilo.com/examples` - These `.py` files are served as `text/x-python; charset=utf-8` with `Content-Disposition: inline`. - Text to music output file: `https://platform.sonilo.com/examples/text-to-music-output.py` - Video to music minimal workflow: `https://platform.sonilo.com/examples/video-to-music-minimal.py` - Video to SFX minimal workflow: `https://platform.sonilo.com/examples/video-to-sfx-minimal.py` - Audio ducking minimal workflow: `https://platform.sonilo.com/examples/audio-ducking.py` - Account usage dashboard: `https://platform.sonilo.com/examples/account-usage-dashboard.py` - Sapient output-file grader helper: `https://platform.sonilo.com/examples/sapient-grader-output-file.py` Common guessed paths: - `/examples/python`, `/docs/examples`, `/docs/smoke-tests`, and `/docs/agent-smoke-tests` redirect to `/examples`. - `/docs/eval`, `/docs/custom-evals`, and `/docs/grader` redirect to `/docs/evals`. - `/docs/vocal-isolation`, `/docs/reference/vocal-isolation`, and `/docs/endpoints/vocal-isolation` redirect to `/docs/api/vocal-isolation`. - `/v1/vocal-isolation` is currently unavailable and is not in the active OpenAPI schema. Do not call it in new integrations; use `/v1/audio-ducking`, `/v1/video-to-sfx`, `/v1/video-to-music`, or `/v1/text-to-music` depending on the workflow. Use `https://platform.sonilo.com/docs/evals` for copyable custom eval prompt and grader guidance, including the Codex/Sapient file-capture requirement.