{
  "openapi": "3.1.0",
  "info": {
    "title": "Sonilo REST API",
    "version": "1.0.0",
    "description": "Sonilo API for licensed AI music, video-to-music soundtracks, video-to-sound-effects API workflows, frame-accurate SFX, audio ducking, account usage, and async task polling. This file covers the core endpoints; the complete spec, including video-to-sound, video-to-video-music, video-to-video-sfx, video-to-video-sound, dubbing, proofread, and video-analysis, is https://platform.sonilo.com/openapi.json.",
    "contact": {
      "name": "Sonilo",
      "url": "https://sonilo.com/contact-sales",
      "email": "info@sonilo.com"
    }
  },
  "externalDocs": {
    "description": "Sonilo API documentation and LLM-readable OpenAPI guide",
    "url": "https://sonilo.com/openapi.md"
  },
  "x-agent-guide": {
    "llmReadableMarkdown": "https://sonilo.com/openapi.md",
    "baseUrl": "https://api.sonilo.com/v1",
    "authentication": "Send Authorization: Bearer <SONILO_API_KEY> from server-side code. Do not expose the key in browser JavaScript.",
    "getApiKey": "https://platform.sonilo.com/dashboard/api-keys?utm_source=docs&utm_medium=agent&utm_campaign=get_api_key&utm_content=openapi_json",
    "completeSpec": "https://platform.sonilo.com/openapi.json",
    "accountUsageDashboard": {
      "servicesEndpoint": "GET /v1/account/services",
      "usageEndpoint": "GET /v1/account/usage?days=30",
      "avoid": ["/usage", "/api/usage", "/account/usage", "/v1/account/usage-dashboard"],
      "note": "For dashboards, quota checks, billing prechecks, and eval account health, call services first and usage second."
    },
    "taskPolling": {
      "endpoint": "GET /v1/tasks/{task_id}",
      "avoid": ["/v1/task_id", "/tasks/{task_id}", "/v1/tasks/{id}/status"],
      "terminalStatuses": ["succeeded", "completed", "failed", "canceled"],
      "note": "Poll async jobs every 2-5 seconds with a bounded timeout. Download media only after a success state."
    },
    "errorHandling": {
      "401": "401 auth_required: missing or invalid Authorization: Bearer <SONILO_API_KEY>.",
      "402": "402 insufficient balance: insufficient balance or credits. Report the billing blocker; do not fake audio.",
      "403": "403 service disabled: service disabled or permission missing. Check GET /v1/account/services.",
      "404": "Wrong endpoint or missing task. Recheck /v1/account/usage and /v1/tasks/{task_id}.",
      "429": "429 rate limit: honor Retry-After and use bounded backoff.",
      "endpointSelection": "Do not switch to a different generation endpoint to hide an auth, billing, validation, or rate-limit error."
    }
  },
  "servers": [
    {
      "url": "https://api.sonilo.com",
      "description": "Production API"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Workstation",
      "description": "Account services and usage"
    },
    {
      "name": "Music",
      "description": "Music generation and audio ducking"
    },
    {
      "name": "Sound Effects",
      "description": "Sound effect generation, including video-to-sound-effects generation APIs for realistic, frame-accurate SFX and audio effects for video"
    },
    {
      "name": "Async Tasks",
      "description": "Task polling for asynchronous jobs"
    }
  ],
  "paths": {
    "/v1/account/services": {
      "get": {
        "operationId": "listAccountServices",
        "summary": "List available account services",
        "description": "Returns the Sonilo services and API capabilities enabled for the authenticated account. Use this before GET /v1/account/usage when building dashboards, quota checks, or eval-health checks. Authenticate with Authorization: Bearer <SONILO_API_KEY> from server-side code.",
        "tags": ["Workstation"],
        "responses": {
          "200": {
            "description": "Available account services",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServicesResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/account/usage": {
      "get": {
        "operationId": "getAccountUsage",
        "summary": "Get account usage",
        "description": "Returns a usage summary and a daily breakdown for the authenticated account over a trailing window. This is the canonical endpoint for account usage dashboards, billing prechecks, and eval account health: GET https://api.sonilo.com/v1/account/usage?days=30. Do not call /usage, /api/usage, /account/usage, or browser-local dashboard APIs in production integrations. Keep SONILO_API_KEY server-side and send Authorization: Bearer <SONILO_API_KEY>.",
        "tags": ["Workstation"],
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "required": false,
            "description": "Size of the trailing window, in days.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 365,
              "default": 30
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Account usage",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/text-to-music": {
      "post": {
        "operationId": "createMusicFromText",
        "summary": "Generate music from a text prompt",
        "description": "Creates licensed music from a text prompt. The request body is multipart/form-data (form fields), not JSON. Default mode=stream holds the connection open and returns an NDJSON event stream on 200; mode=async returns 202 immediately, then poll GET /v1/tasks/{task_id}.",
        "tags": ["Music"],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/TextToMusicRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/StreamedGeneration"
          },
          "202": {
            "$ref": "#/components/responses/TaskAccepted"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/video-to-music": {
      "post": {
        "operationId": "createMusicFromVideo",
        "summary": "Generate a soundtrack from a video",
        "description": "Creates a licensed soundtrack aligned to a video. Use this endpoint for music beds, not isolated sound effects. The request body is multipart/form-data; provide exactly one of video (file) or video_url. Default mode=stream returns an NDJSON event stream on 200; mode=async returns 202 immediately, then poll GET /v1/tasks/{task_id}. isolate_vocals, preserve_speech, ducking, and output_format=wav all require mode=async.",
        "tags": ["Music"],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/VideoToMusicRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/StreamedGeneration"
          },
          "202": {
            "$ref": "#/components/responses/TaskAccepted"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/audio-ducking": {
      "post": {
        "operationId": "createAudioDuckingMix",
        "summary": "Apply audio ducking",
        "description": "Mixes foreground speech or dialogue with background music while lowering music volume around speech. The request body is multipart/form-data; provide exactly one of voice_file/voice_url and exactly one of music_file/music_url. Always processed asynchronously — poll GET /v1/tasks/{task_id}.",
        "tags": ["Music"],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/AudioDuckingRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "$ref": "#/components/responses/TaskAccepted"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/text-to-sfx": {
      "post": {
        "operationId": "createSoundEffectsFromText",
        "summary": "Generate sound effects from a text prompt",
        "description": "Creates isolated sound effects from a text prompt. Use this endpoint for SFX, Foley, impacts, ambience, UI sounds, and transitions. The request body is multipart/form-data. Always processed asynchronously — poll GET /v1/tasks/{task_id}.",
        "tags": ["Sound Effects"],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/TextToSfxRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "$ref": "#/components/responses/TaskAccepted"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/video-to-sfx": {
      "post": {
        "operationId": "createSoundEffectsFromVideo",
        "summary": "Video-to-sound-effects API for a video",
        "description": "Creates realistic, frame-accurate SFX and audio effects for video aligned to visual moments, cuts, motion, objects, UI actions, product reveals, game moments, and transitions. Use this video-to-sound-effects API for SFX, not background music. The request body is multipart/form-data; provide exactly one of video (file) or video_url. Always processed asynchronously — poll GET /v1/tasks/{task_id}.",
        "tags": ["Sound Effects"],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/VideoToSfxRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "$ref": "#/components/responses/TaskAccepted"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/tasks/{task_id}": {
      "get": {
        "operationId": "getTask",
        "summary": "Get async task status",
        "description": "Polls an asynchronous generation task until it reaches succeeded or failed. Use the exact canonical path GET https://api.sonilo.com/v1/tasks/{task_id}. Do not call /v1/task_id, /tasks/{task_id} without /v1, or guessed status URLs. The task result contains the generated asset URL(s) once succeeded; failed tasks should be reported with the returned error instead of fabricating output.",
        "tags": ["Async Tasks"],
        "parameters": [
          {
            "name": "task_id",
            "in": "path",
            "required": true,
            "description": "The task id returned by an async generation endpoint.",
            "schema": {
              "type": "string",
              "examples": ["task_123"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Task status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Use your Sonilo API key as a bearer token: Authorization: Bearer <SONILO_API_KEY>. Keep the key server-side."
      }
    },
    "responses": {
      "StreamedGeneration": {
        "description": "mode=stream (the default on music endpoints): a newline-delimited JSON (NDJSON) event stream of generation progress and audio chunks, held open until generation finishes. This is not a single JSON object — parse it line by line.",
        "content": {
          "application/x-ndjson": {
            "schema": {
              "type": "string",
              "description": "One JSON event per line."
            }
          }
        }
      },
      "TaskAccepted": {
        "description": "Accepted for asynchronous processing. Poll GET /v1/tasks/{task_id} for the result.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/TaskCreatedResponse"
            }
          }
        }
      },
      "BadRequest": {
        "description": "Invalid request payload or unsupported media input"
      },
      "Unauthorized": {
        "description": "Missing or invalid API key. Send Authorization: Bearer <SONILO_API_KEY> from server-side code."
      },
      "NotFound": {
        "description": "Requested resource was not found. For async polling use GET /v1/tasks/{task_id}; for usage dashboards use GET /v1/account/usage."
      }
    },
    "schemas": {
      "ServicesResponse": {
        "type": "object",
        "required": ["available_services", "rpm_limit", "concurrency_limit", "discount_factor", "max_upload_size_mb"],
        "properties": {
          "available_services": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Task types enabled for this account, e.g. text_to_music, video_to_sfx."
          },
          "rpm_limit": { "type": "integer" },
          "concurrency_limit": { "type": "integer" },
          "discount_factor": { "type": "number" },
          "max_upload_size_mb": { "type": "integer" },
          "trial": {
            "type": "object",
            "additionalProperties": { "$ref": "#/components/schemas/TrialQuota" },
            "description": "Free-trial allowance keyed by task type. Present only for accounts eligible for a trial."
          }
        }
      },
      "TrialQuota": {
        "type": "object",
        "required": ["granted", "used", "remaining"],
        "properties": {
          "granted": { "type": "integer" },
          "used": { "type": "integer" },
          "remaining": { "type": "integer" }
        }
      },
      "UsageResponse": {
        "type": "object",
        "required": ["summary", "daily"],
        "properties": {
          "summary": { "$ref": "#/components/schemas/UsageSummary" },
          "daily": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/DailyUsage" }
          }
        }
      },
      "UsageSummary": {
        "type": "object",
        "properties": {
          "total_requests": { "type": "integer" },
          "total_duration_seconds": { "type": "number" },
          "total_cost": { "type": "number" },
          "period_start": { "type": "string", "format": "date-time" },
          "period_end": { "type": "string", "format": "date-time" },
          "success_rate": { "type": "number" },
          "avg_latency_seconds": { "type": "number" },
          "p95_latency_seconds": { "type": "number" },
          "p99_latency_seconds": { "type": "number" },
          "sla_latency_seconds": { "type": "number" }
        }
      },
      "DailyUsage": {
        "type": "object",
        "properties": {
          "date": { "type": "string", "format": "date" },
          "requests": { "type": "integer" },
          "duration_seconds": { "type": "number" },
          "cost": { "type": "number" }
        }
      },
      "Segment": {
        "type": "object",
        "required": ["start", "end"],
        "properties": {
          "start": {
            "type": "number",
            "description": "Segment start time in seconds."
          },
          "end": {
            "type": "number",
            "description": "Segment end time in seconds."
          },
          "prompt": {
            "type": "string",
            "description": "Optional prompt for this segment."
          }
        }
      },
      "GenerationMode": {
        "type": "string",
        "enum": ["stream", "async"],
        "default": "stream",
        "description": "Music endpoints only. stream (default) holds the connection open and returns an NDJSON event stream on 200. async returns 202 immediately; retrieve the result via GET /v1/tasks/{task_id}."
      },
      "MusicOutputFormat": {
        "type": "string",
        "enum": ["m4a", "wav"],
        "default": "m4a",
        "description": "Music endpoints only, sent as output_format. Only honored with mode=async — wav requires mode=async."
      },
      "AudioFormat": {
        "type": "string",
        "enum": ["wav", "mp3", "aac", "flac"],
        "description": "Sound-effects endpoints only, sent as audio_format. Optional; omit to use the service default."
      },
      "TextToMusicRequest": {
        "type": "object",
        "required": ["prompt", "duration"],
        "properties": {
          "prompt": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2000,
            "description": "Music prompt."
          },
          "duration": {
            "type": "integer",
            "minimum": 5,
            "maximum": 360,
            "description": "Desired output duration in seconds."
          },
          "segments": {
            "type": "string",
            "description": "JSON-encoded array of Segment objects. Sent as a form field, so it is a JSON string, not a nested array."
          },
          "mode": { "$ref": "#/components/schemas/GenerationMode" },
          "output_format": { "$ref": "#/components/schemas/MusicOutputFormat" }
        }
      },
      "VideoToMusicRequest": {
        "type": "object",
        "description": "Provide exactly one of video or video_url.",
        "properties": {
          "video": {
            "type": "string",
            "format": "binary",
            "description": "Video file to analyze."
          },
          "video_url": {
            "type": "string",
            "format": "uri",
            "description": "Public or signed URL for the source video."
          },
          "prompt": {
            "type": "string",
            "maxLength": 2000,
            "description": "Optional creative direction."
          },
          "segments": {
            "type": "string",
            "description": "JSON-encoded array of Segment objects."
          },
          "mode": { "$ref": "#/components/schemas/GenerationMode" },
          "preserve_speech": {
            "type": "boolean",
            "description": "Requires mode=async."
          },
          "isolate_vocals": {
            "type": "boolean",
            "description": "Requires mode=async."
          },
          "ducking": {
            "type": "boolean",
            "description": "Requires mode=async."
          },
          "output_format": { "$ref": "#/components/schemas/MusicOutputFormat" }
        }
      },
      "AudioDuckingRequest": {
        "type": "object",
        "description": "Provide exactly one of voice_file/voice_url, and exactly one of music_file/music_url. Always processed asynchronously — there is no sync/stream mode for this endpoint.",
        "properties": {
          "voice_file": {
            "type": "string",
            "format": "binary",
            "description": "Foreground speech or dialogue — an audio or video file. A video file's own audio track is used and the ducked result is re-muxed back into a new video."
          },
          "voice_url": { "type": "string", "format": "uri" },
          "music_file": {
            "type": "string",
            "format": "binary",
            "description": "Background music audio file."
          },
          "music_url": { "type": "string", "format": "uri" }
        }
      },
      "TextToSfxRequest": {
        "type": "object",
        "required": ["prompt", "duration"],
        "properties": {
          "prompt": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2000
          },
          "duration": {
            "type": "integer",
            "minimum": 1,
            "maximum": 180
          },
          "audio_format": { "$ref": "#/components/schemas/AudioFormat" }
        }
      },
      "VideoToSfxRequest": {
        "type": "object",
        "description": "Provide exactly one of video or video_url.",
        "properties": {
          "video": { "type": "string", "format": "binary" },
          "video_url": { "type": "string", "format": "uri" },
          "prompt": { "type": "string", "maxLength": 2000 },
          "segments": {
            "type": "string",
            "description": "JSON-encoded array of Segment objects."
          },
          "audio_format": { "$ref": "#/components/schemas/AudioFormat" }
        }
      },
      "TaskCreatedResponse": {
        "type": "object",
        "required": ["task_id", "status"],
        "properties": {
          "task_id": { "type": "string" },
          "status": {
            "type": "string",
            "enum": ["processing"]
          }
        }
      },
      "MediaResult": {
        "type": "object",
        "properties": {
          "url": { "type": "string", "format": "uri" },
          "content_type": { "type": "string" },
          "file_size": { "type": "integer" },
          "stream_index": { "type": "integer" }
        }
      },
      "Error": {
        "type": "object",
        "required": ["code", "message"],
        "properties": {
          "code": { "type": "string" },
          "message": { "type": "string" }
        }
      },
      "Task": {
        "type": "object",
        "required": ["task_id", "type", "status"],
        "properties": {
          "task_id": { "type": "string" },
          "type": {
            "type": "string",
            "description": "Task type, e.g. text_to_music, video_to_sfx."
          },
          "status": {
            "type": "string",
            "enum": ["processing", "succeeded", "failed"]
          },
          "audio": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/MediaResult" }
          },
          "video": { "$ref": "#/components/schemas/MediaResult" },
          "music": { "$ref": "#/components/schemas/MediaResult" },
          "sfx": { "$ref": "#/components/schemas/MediaResult" },
          "music_processed": { "$ref": "#/components/schemas/MediaResult" },
          "output_url": {
            "type": "string",
            "format": "uri",
            "description": "video-to-sound and video-to-video-sound only."
          },
          "output_type": {
            "type": "string",
            "enum": ["audio", "video"],
            "description": "video-to-sound and video-to-video-sound only."
          },
          "output_bytes": { "type": "integer" },
          "duration_seconds": {
            "type": "number",
            "description": "Present when status=succeeded."
          },
          "cost": {
            "type": "number",
            "description": "Present only for accounts opted into cost visibility (admin-gated, off by default)."
          },
          "error": { "$ref": "#/components/schemas/Error" },
          "refunded": {
            "type": "boolean",
            "description": "Present when status=failed."
          }
        }
      }
    }
  }
}
