Quickstart

Make your first Sonilo API request in under five minutes.

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.

Sample stream
{"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.

generate_music.py
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.