API v1
Documentation
Singing voice conversion for whole songs: a track and a voice sample go in, the same track in the new voice comes out, mixed and mastered.
Overview
The API is asynchronous. One conversion is one job, and a job goes through four steps:
- create the job and receive two upload links;
- upload the track and the voice sample to those links;
- start processing;
- poll the status until the job is done, then download the result.
Base URL: https://s1solutions.pro. Requests and responses are JSON. A typical track takes about two minutes to process.
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/jobs | Create a job |
| POST | /v1/jobs/{id}/start | Start processing |
| GET | /v1/jobs/{id} | Status and result |
| GET | /v1/balance | Balance |
Authentication
Every request to /v1/… carries your API key in a header:
Authorization: Bearer <api key>
Create keys in the dashboard. A key is shown once, so save it right away. A revoked key stops working immediately. Uploads and the result download use signed links and do not need the header.
POST /v1/jobs
Creates a job and returns two upload links.
Request body
| Field | Type | Description |
|---|---|---|
format | string | Result format: "mp3" or "wav". |
{"format": "mp3"}
Response
{
"id": "0b6f0c1e-7c0a-4a52-9d1b-2f4d5c1f8a11",
"upload": {
"track_url": "https://…",
"voice_url": "https://…"
}
}
track_url is for the song and voice_url is for the voice sample. Both links are valid for 30 minutes. A job that is never started eventually gets the status expired.
Upload files
Send each file with an HTTP PUT to its link: raw bytes in the request body, no multipart form and no Authorization header.
curl -T song.mp3 "<track_url>"
curl -T voice.wav "<voice_url>"
Each file can be up to 200 MB.
POST /v1/jobs/{id}/start
Puts an uploaded job in the queue. No request body. The response is the job object, the same as in the status request.
{
"id": "0b6f0c1e-7c0a-4a52-9d1b-2f4d5c1f8a11",
"status": "queued",
"format": "mp3",
"created_at": "2026-10-02T12:00:00+00:00",
"queue_position": 0,
"estimated_sec": 120
}
Errors
| Code | Reason | What to do |
|---|---|---|
400 | A file is not uploaded | Upload both the track and the voice sample, then call start again. |
402 | Insufficient balance | The balance must cover the reserve. See Billing. |
503 | Service busy | The queue is too long. The response includes estimated_wait_sec, the estimated wait in seconds. Retry later: the job stays in awaiting_upload and nothing is reserved. |
GET /v1/jobs/{id}
Returns the job object. id, status, format and created_at are always present; the other fields depend on the status.
| Status | Meaning | Extra fields |
|---|---|---|
awaiting_upload | Created, waiting for files and start | — |
queued | In the queue | queue_position (0 means next), estimated_sec (estimated time until done) |
running | Being processed | estimated_sec |
done | Finished | track_sec (source track length), cost (amount charged, in US dollars), result_url, result_expires_in_sec |
failed | Processing failed, nothing charged | error (the reason) |
expired | The job was not started in time | — |
{
"id": "0b6f0c1e-7c0a-4a52-9d1b-2f4d5c1f8a11",
"status": "done",
"format": "mp3",
"created_at": "2026-10-02T12:00:00+00:00",
"track_sec": 212.4,
"cost": 0.36,
"result_url": "https://…",
"result_expires_in_sec": 287
}
The result link works for 5 minutes. When the window closes, result_url becomes null, the file is deleted and it cannot be downloaded later. Poll every few seconds and download the result as soon as the job is done.
GET /v1/balance
Your current balance in US dollars. Amounts reserved for jobs in progress are not included.
{"balance": 25.0}
Billing
The price is from $0.10 per minute of the source track, billed per second. Your own rate is agreed with us and shown in your account. A 3-minute track costs $0.30. The length of the voice sample does not affect the price.
The balance, the cost field and the /v1/balance response are in US dollars. There is no online payment: the balance is topped up by arrangement. Contact us to get access and add credit.
- Start. The cost of a maximum-length track is reserved on your balance: 10 min × $0.10 = $1.00. If the balance cannot cover the reserve, start returns
402. - Success. The actual cost is charged, based on the length of the source track, and the reserve is released.
- Failure. The whole reserve is released. Jobs with the status
failedcost nothing.
Every movement — top-ups, reserves, releases and charges — is listed in the balance history in the dashboard.
Limits and retention
- Track length: up to 10 minutes.
- File size: up to 200 MB per file.
- Upload links are valid for 30 minutes.
- The result link works for 5 minutes. After that the result cannot be retrieved.
- Files are not kept: the track and the voice sample are deleted when the job ends, and the result is deleted when its link expires.
Errors
Errors come back with the matching HTTP status and a JSON body with a text description.
| Code | When |
|---|---|
400 | Bad request: unknown format, or a file was not uploaded before start |
401 | The key is missing, wrong or revoked |
402 | The balance cannot cover the reserve |
404 | Job not found |
409 | The job is already started or finished |
429 | Too many jobs created but not started |
503 | Service busy; the body includes estimated_wait_sec |
Example: curl
The full path from a new job to a finished file.
export S1_API="https://s1solutions.pro"
export S1_KEY="YOUR_API_KEY"
# 1. Create a job
curl -s -X POST "$S1_API/v1/jobs" \
-H "Authorization: Bearer $S1_KEY" \
-H "Content-Type: application/json" \
-d '{"format": "mp3"}'
# → {"id": "…", "upload": {"track_url": "…", "voice_url": "…"}}
# Fill these in from the response
export JOB_ID="…"
export TRACK_URL="…"
export VOICE_URL="…"
# 2. Upload the track and the voice sample (HTTP PUT, raw bytes)
curl -f -T song.mp3 "$TRACK_URL"
curl -f -T voice.wav "$VOICE_URL"
# 3. Start
curl -s -X POST "$S1_API/v1/jobs/$JOB_ID/start" \
-H "Authorization: Bearer $S1_KEY"
# → {"id": "…", "status": "queued", "queue_position": 0, "estimated_sec": 120, …}
# 4. Poll until status is "done"
curl -s "$S1_API/v1/jobs/$JOB_ID" \
-H "Authorization: Bearer $S1_KEY"
# → {"id": "…", "status": "done", "result_url": "…", "result_expires_in_sec": 287, …}
# 5. Download the result: the link works for 5 min
curl -f -o result.mp3 "<result_url>"
# Balance
curl -s "$S1_API/v1/balance" -H "Authorization: Bearer $S1_KEY"
# → {"balance": 25.0}
Example: Python
Needs the requests library. The function creates a job, uploads both files, starts processing, waits until the job is done and saves the result.
import time
import requests
API = "https://s1solutions.pro"
KEY = "YOUR_API_KEY"
HEADERS = {"Authorization": f"Bearer {KEY}"}
def convert(track_path, voice_path, out_path, fmt="mp3"):
# 1. Create a job
r = requests.post(f"{API}/v1/jobs", json={"format": fmt}, headers=HEADERS, timeout=30)
r.raise_for_status()
job = r.json()
job_id = job["id"]
# 2. Upload the files: HTTP PUT, raw bytes
uploads = (
(job["upload"]["track_url"], track_path),
(job["upload"]["voice_url"], voice_path),
)
for url, path in uploads:
with open(path, "rb") as f:
requests.put(url, data=f, timeout=600).raise_for_status()
# 3. Start; if the service is busy (503), wait and retry
while True:
r = requests.post(f"{API}/v1/jobs/{job_id}/start", headers=HEADERS, timeout=30)
if r.status_code != 503:
break
time.sleep(30)
if r.status_code == 402:
raise RuntimeError("Insufficient balance")
r.raise_for_status()
# 4. Wait until the job is done
while True:
r = requests.get(f"{API}/v1/jobs/{job_id}", headers=HEADERS, timeout=30)
r.raise_for_status()
job = r.json()
if job["status"] == "done":
break
if job["status"] in ("failed", "expired"):
raise RuntimeError(f"Job {job_id}: {job.get('error') or job['status']}")
time.sleep(5)
# 5. Download the result right away: the link works for 5 min
with requests.get(job["result_url"], stream=True, timeout=600) as r:
r.raise_for_status()
with open(out_path, "wb") as f:
for chunk in r.iter_content(chunk_size=1 << 20):
f.write(chunk)
return job
if __name__ == "__main__":
job = convert("song.mp3", "voice.wav", "result.mp3")
print(f"Done: {job['track_sec']} s, charged ${job['cost']}")
balance = requests.get(f"{API}/v1/balance", headers=HEADERS, timeout=30).json()["balance"]
print(f"Balance: ${balance}")