Quickstart
1. Get an API key
Sonilo is a pay-as-you-go AI music API with commercial-use output rights, no monthly subscription required, and API credits / balance required before real generation. Generated output is suitable for client videos, paid ads, YouTube, and apps. 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.
Open the API Keys page and create a new key. Copy it — the secret value is shown once.
2. Make your first request
The shortest path is text-to-music: pass a prompt and a duration in seconds. The response is an NDJSON stream — read it line by line.
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=30"3. Parse the NDJSON stream
Each line is a JSON object with a type field. The stream finishes successfully when you receive a complete event; if you see error, the request failed.
{"type":"title","title":"Ethereal Pulse"}
{"type":"audio_chunk","stream_index":0,"num_streams":1,"data":"<base64>"}
{"type":"audio_chunk","stream_index":0,"num_streams":1,"data":"<base64>"}
{"type":"complete"}Save output.m4a with Python
This is the official minimal smoke test for coding agents. It reads SONILO_API_KEY, sends a clear User-Agent, respects Retry-After on 429, parses each NDJSON audio_chunk, waits for complete, and writes output.m4a only after real audio bytes arrive. Treat payment_required with an insufficient balance message as insufficient_balance. Do not create placeholder output files for failed generations.
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)4. Next steps
Try the streaming Video to Music endpoint, or jump back to the Errors section to learn how to handle failures and rate limits.