API errors
Error format
Non-streaming errors return JSON with a stable code and a human-readable message. Use the HTTP status for retry and billing logic; use code for user-facing handling.
{
"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 [email protected]."
}Status codes
| Code | Condition |
|---|---|
| 400 | Invalid request shape, conflicting inputs, missing required fields, or unsupported parameters. |
| 401 | API key is missing, invalid, revoked, or not sent as Authorization: Bearer sk_your_api_key. |
| 402 | Branch on the code field. trial_exhausted means the free trial for that service is spent and the account has never been funded — add a payment method. insufficient_balance means a funded wallet ran out — add balance and retry. payment_required covers the remaining cases, such as a suspended account or an exceeded credit limit. |
| 403 | API key is valid, but the account or workspace cannot access this endpoint. |
| 413 | Uploaded file exceeds the endpoint size limit. |
| 422 | The input is syntactically valid but cannot be processed, such as an over-limit duration or a source with no video track. |
| 429 | Requests-per-minute limit or concurrent-generation limit exceeded. The message says which one, so a client can tell whether to slow down or to wait for a slot. Both limits can be raised — contact [email protected]. |
| 502 | Upstream generation provider failed or timed out. Retry the request. |
Rate limit errors
Two separate limits return 429 with rate_limit_exceeded, and they call for different handling. The message distinguishes them; the code is the same for both.
The per-minute limit — slow down, then retry:
{
"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 [email protected]."
}The concurrency limit — wait for a running generation to finish, since no amount of waiting frees a slot on its own:
{
"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 [email protected]."
}The numbers in both messages are your account's actual limits — the values above are the standard-tier defaults. See GET /v1/account/services to read them programmatically, and email [email protected] to have either raised.
Video input errors
Every endpoint that takes video or video_url — /v1/video-to-music, /v1/video-to-video-music, /v1/video-to-sfx, /v1/video-to-video-sfx, /v1/video-to-sound, /v1/video-to-video-sound, and /v1/dubbing — requires a source that carries a real video stream. An audio-only file returns 422 before anything is generated or billed.
{
"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."
}The container matters less than what is inside it. Renaming an .m4a or .mp3 to .mp4does 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. Send a different file; retrying the same one cannot succeed.
A source that does carry video but runs past the endpoint's limit returns the same status with a different message, naming both durations:
{
"code": "unprocessable_entity",
"message": "Video duration 370.0s exceeds maximum of 360s"
}Eval verification
For generation smoke tests, mark the run as passed only when the expected output file exists and is larger than 0 bytes. For the text-to-music minimal workflow, require output.m4a after the complete stream event. Do not create placeholder media files after failed API calls.
When HTTP 402 returns trial_exhausted, the account has used every free trial call for that service and has never added funds. Prompt the user to add a payment method rather than retrying. When it returns insufficient_balance, a funded wallet ran dry — add balance and retry the same request. Branch on code, never on the message text.
Authentication failures
Send Authorization: Bearer sk_your_api_key on every API request to https://api.sonilo.com/v1. Store real keys server-side, commonly as SONILO_API_KEY.
curl https://api.sonilo.com/v1/account/services \
-H "Authorization: Bearer sk_your_api_key"Use 401 for missing or invalid credentials. Use 403 for a valid key that lacks access to the requested endpoint or workspace. In coding-agent evals, classify these as auth_invalid and forbidden, not as Sonilo endpoint bugs.
Streaming errors
/v1/text-to-music and /v1/video-to-music stream application/x-ndjson. Read one JSON object per line. If an event has type: "error", stop reading and handle the included code and message.
{
"type": "error",
"code": "upstream_error",
"message": "Upstream processing error. Please retry."
}Async task errors
/v1/text-to-sfx, /v1/video-to-sfx, and /v1/audio-ducking return 202 with a task_id. Poll GET /v1/tasks/{task_id} until the task is succeeded or failed.
{
"task_id": "9f5f2f7e-...",
"status": "failed",
"error": {
"code": "upstream_error",
"message": "Upstream processing error. Please retry."
},
"refunded": true
}Retry guidance
- Retry
429and transient502failures with exponential backoff. When the response includesRetry-After, wait at least that many seconds before the next attempt. - For the per-minute limit specifically, the counter runs on a fixed 60-second window and every request counts, rejected ones included. A retry inside the same window cannot succeed, which is why the message asks for a full minute: that clears the window whatever your phase within it.
- Do not retry
400,401,403, or422until the request, key, permissions, or input changes. - Use
404/not_foundto catch wrong hosts or paths. Requests must go tohttps://api.sonilo.com/v1, not docs URLs such ashttps://platform.sonilo.com/docs/api/video-to-sfxor local proxy routes such as/api/dashboard. - For endpoint parameters and examples, start from the API reference.