On this page
Getting started
Alignment
Other products
Reference
Tonael API
Submit an MP3 audio file with pre-segmented lyrics and receive word-level timestamps for karaoke synchronization. Results are delivered asynchronously and retained per tier: 24 hours for hot (real-time) jobs, 72 hours for batch jobs.
https://tonael.com
Authentication
All API requests require an API key sent via the X-API-Key header.
X-API-Key: ton_your_api_key_here
API keys are provided by the platform administrator. Keys prefixed with ton_ are Tonael keys.
Free Tier & Sign-up
Start free — 10 free minutes of tonael, no card required. Add a
card and your allowance grows to 30 free minutes. The grant lands as
credit on your balance, so you choose which model to spend it on — the minutes quoted are at the
tonael hot rate, so tonael_ultra draws
it down faster and batch stretches it further. Free-tier keys run on hot
(real-time) processing with a small concurrency cap; making your first top-up lifts those limits and unlocks
batch processing.
POST /api/v1/signup
Create an account from an email address. The API key is returned exactly once. Your free minutes unlock after you verify your email.
curl -X POST https://tonael.com/api/v1/signup \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com"}'
GET /api/v1/verify
Redeem the verification token from your sign-up email. Verifying your email releases the free credit.
curl "https://tonael.com/api/v1/verify?token=your_verification_token"
Pricing & Metering
You are billed per second of audio, rounded up, with a 30-second minimum per job. Prices are in USD, exclusive of VAT/sales tax.
| Model | Hot (real-time) | Batch (24h window, −37.5%) |
|---|---|---|
tonaelword-level alignment |
$0.045 / min | $0.028 / min |
tonael_ultravocal isolation + alignment |
$0.095 / min | $0.059 / min |
A 3.5-minute song costs from $0.16 (tonael) or $0.33 (tonael_ultra) at hot rates. Batch pricing applies a minimum pool of 60 audio-minutes per submission window. Processing more than 10,000 minutes/month? Talk to us.
Failed jobs are never charged. If a job fails or is cancelled, the reserved amount is automatically re-credited to your balance.
Credits
Credit is prepaid. Buy a pack — $15, $50, or $200 — or top up a custom amount. Minimum top-up: $15. Jobs draw down your balance as they are metered.
GET /api/v1/credits/balance
Return the current balance in dollars.
curl https://tonael.com/api/v1/credits/balance \
-H "X-API-Key: ton_your_api_key_here"
{ "balance_usd": "42.50" }
POST /api/v1/credits/topup
Charge your registered card for a credit pack (pack_15, pack_50, pack_200) or a custom amount_usd (minimum 15). Provide exactly one of the two.
Before your first top-up, set your billing details once via POST /api/v1/credits/tax-identity (country, is_business, and for an EU business an optional vat_number). They determine the VAT on your invoice, so a top-up before they are set returns 422 TAX_IDENTITY_REQUIRED.
An Idempotency-Key header is required. Generate one value per purchase (a UUID is ideal) and reuse that same value if you retry a request that timed out or failed with a network error — that is what guarantees your card is charged only once. A repeat of a key that already went through returns 409 DUPLICATE_SUBMISSION instead of charging again.
curl -X POST https://tonael.com/api/v1/credits/topup \
-H "X-API-Key: ton_your_api_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8f14e45f-ea3f-4d1b-9a2c-7b6c0d5e1a90" \
-d '{"pack": "pack_50"}'
{ "status": "credited", "amount_usd": "$50.00", "balance_usd": "92.50" }
Credits are prepaid and non-refundable, and never expire. Failed or cancelled jobs are automatically re-credited to your balance; no cash refunds are issued.
POST /api/v1/sync
Submit an alignment job. Returns immediately with a job ID. Results are delivered asynchronously.
Request
Content-Type: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
audio |
file | One of | Audio file — MP3, WAV, FLAC, M4A/AAC, OGG/Opus, AIFF, WMA, WebM and more (max 1 GB, max 3 hours). Provide this or audio_url. |
audio_url |
string | One of | HTTPS URL we download the audio from instead of you uploading it. Same formats and limits. Recommended for large files or slow connections — a direct upload ties up the request for as long as your link takes, whereas a URL is fetched server-to-server in seconds. Signed / expiring URLs work. The host must be publicly reachable over HTTPS. |
lyrics |
string | Yes | Lyrics text (max 240,000 chars). Auto-segmented if no line breaks provided. |
language |
string | Yes | ISO 639-1 language code: fr, en, es, de, it, pt, ja, ko |
model |
string | No | tonael (default) or tonael_ultra. Tonael Ultra delivers better results than Tonael, at a higher cost. Try Tonael first; switch to Tonael Ultra if the result is not good enough. |
granularity |
string | No | char (default) or word. The default char returns word timestamps plus a nested chars array of per-letter timestamps on each word. Pass word for a lighter, word-only response. Same price either way — granularity does not change billing. |
priority |
string | No | hot (default, real-time) or batch (lower cost, opportunistic processing, always resolved within a 24-hour window — see batch_fallback for what happens at the end of it). See Batch processing. Batch requires a paid account. |
batch_fallback |
string | No | Batch jobs only. What happens if the job is still unprocessed at the end of the 24-hour batch window: reject (default — the job is rejected and fully refunded) or hot (the job automatically switches to hot processing, billed at the hot rate). The value is always validated (an invalid value is a 400, even on hot submissions) but has no effect outside batch. See Batch processing. |
callback_url |
string | No | HTTPS URL notified when the job completes. Deliveries are HMAC-SHA256 signed — see Webhooks. If omitted, poll the status endpoint instead. |
Example
curl -X POST https://tonael.com/api/v1/sync \
-H "X-API-Key: ton_your_api_key_here" \
-F "audio=@song.mp3" \
-F "lyrics=Joyeux anniversaire
Cher Antoine
On te souhaite
Plein de bonheur" \
-F "language=fr"
Response 202 Accepted
{
"job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "processing",
"created_at": "2026-04-04T10:30:00Z"
}
GET /api/v1/sync/{job_id}
Retrieve the status and result of an alignment job. Results are available for 24 hours after completion for hot jobs, and 72 hours for batch jobs. After that window the endpoint returns 410 Gone.
Query Parameters
| Param | Default | Description |
|---|---|---|
format |
json |
Output format. Options: json, lrc, srt, vtt, ass, ttml, sbv, txt |
Export Examples
# Get as Enhanced LRC (karaoke)
curl "https://tonael.com/api/v1/sync/{job_id}?format=lrc" -H "X-API-Key: ..."
# Get as SRT (subtitles)
curl "https://tonael.com/api/v1/sync/{job_id}?format=srt" -H "X-API-Key: ..."
# Get as WebVTT (web video)
curl "https://tonael.com/api/v1/sync/{job_id}?format=vtt" -H "X-API-Key: ..."
# Get as ASS (video karaoke with word highlighting)
curl "https://tonael.com/api/v1/sync/{job_id}?format=ass" -H "X-API-Key: ..."
Example
curl https://tonael.com/api/v1/sync/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-H "X-API-Key: ton_your_api_key_here"
Response 200 OK (completed)
{
"job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "completed",
"language": "fr",
"model": "tonael",
"product_type": "sync_tonael",
"created_at": "2026-04-04T10:30:00Z",
"completed_at": "2026-04-04T10:30:12Z",
"expires_at": "2026-04-05T10:30:12Z",
"result": {
"processing": { "processing_time_ms": 3437 },
"result": {
"segments": [
{
"index": 0,
"text": "Joyeux anniversaire",
"start": 5.240,
"end": 7.810,
"words": [
{ "word": "Joyeux", "start": 5.240, "end": 5.880, "confidence": 0.94 },
{ "word": "anniversaire", "start": 5.920, "end": 7.810, "confidence": 0.89 }
]
}
],
"statistics": {
"total_segments": 4,
"total_words": 12,
"avg_confidence": 0.87,
"low_confidence_words": 1,
"unaligned_words": 0,
"duration_s": 30.5
}
}
}
}
Per-letter timestamps (default)
By default (no granularity param), each word carries both its own timestamps and a nested chars array of per-letter timestamps. Pass granularity=word for a lighter, word-only response with no chars array. Word-level fields are identical either way, and the price is the same.
{
"word": "café",
"start": 5.240,
"end": 5.880,
"confidence": 0.94,
"chars": [
{ "char": "c", "start": 5.240, "end": 5.360, "confidence": 0.95 },
{ "char": "a", "start": 5.360, "end": 5.520, "confidence": 0.93 },
{ "char": "f", "start": 5.520, "end": 5.700, "confidence": 0.92 },
{ "char": "é", "start": 5.700, "end": 5.880, "confidence": 0.90 }
]
}
Notes: character timestamps are approximately 20 ms in resolution. Some characters — punctuation, and certain diacritics or accents — may not receive an independent timestamp, so a word's chars array can be shorter than its letter count. Characters are always returned in reading order with non-decreasing start times within a word.
Response 202 Accepted (still processing)
{
"job_id": "a1b2c3d4-...",
"status": "processing",
"created_at": "2026-04-04T10:30:00Z"
}
Response 410 Gone (expired)
{
"error": {
"code": "RESULT_EXPIRED",
"message": "Result has expired and is no longer available",
"details": "Results are retained for 24 hours (72 hours for batch jobs)"
}
}
DELETE /api/v1/sync/{job_id}
Cancel a job that is still in the queue. Only jobs with status queued can be cancelled. Jobs that are already processing, completed, or failed cannot be cancelled.
Example
curl -X DELETE https://tonael.com/api/v1/sync/{job_id} \
-H "X-API-Key: ton_your_api_key_here"
Response 200 OK
{ "job_id": "...", "status": "cancelled" }
Response 409 Conflict (not cancellable)
{ "error": { "code": "NOT_CANCELLABLE", "message": "Job is 'processing' — only 'queued' jobs can be cancelled" } }
Batch Processing
Not in a hurry? Submit with priority=batch for a
37.5% lower rate ($0.028/min tonael, $0.059/min tonael_ultra). Batch is
opportunistic: your job waits for a low-cost processing opportunity for up to
24 hours, and every job is resolved by the end of that window
— processed, or rejected and fully refunded. Batch is available on paid accounts; make your first top-up to unlock it.
- Same endpoint (
POST /api/v1/sync), same request shape — just addpriority=batch. - The cost is held from your balance at submission (batch rate), exactly as for hot jobs — insufficient balance is rejected synchronously with
INSUFFICIENT_CREDIT. - Batch is not guaranteed processing. If no processing opportunity arises within the 24-hour batch window, the default resolution is a rejection with a full refund: the job fails with error code
BATCH_WINDOW_ELAPSED, nothing is charged, and you can resubmit hot (immediate) or batch again (same risk). - Prefer never being rejected? Pass
batch_fallback=hot: if the 24-hour window elapses, the job automatically switches to the hot lane and is billed at the hot rate (the price difference is charged when the switch happens; if your balance cannot cover it, the job falls back to the default rejection + full refund). An escalated job is a hot job from that point on: hot rate, hot result retention (24 hours). - Batch results are retained 72 hours after completion (hot: 24 hours).
- Provide a
callback_urlto be notified on completion or rejection, or poll the status endpoint.
curl -X POST https://tonael.com/api/v1/sync \
-H "X-API-Key: ton_your_api_key_here" \
-F "audio=@song.mp3" \
-F "lyrics=..." \
-F "language=fr" \
-F "priority=batch" \
-F "batch_fallback=hot" \
-F "callback_url=https://your-app.example.com/hooks/tonael"
Stem separation
Split a track into isolated stems. A separate product from alignment — billed on its own, per minute of source audio. No lyrics needed.
POST /api/v1/stems
| Field | Values | Notes |
|---|---|---|
audio | MP3 or WAV | the file to separate |
stems_mode | full, vocal_instrumental | four tracks, or vocals + instrumental. Sets the price. |
output_format | mp3, wav | MP3 320 kbps, or PCM 16-bit |
Separation runs at one quality — the best one. There is no model choice:
tonael and
tonael_ultra are accepted for
compatibility and produce the same result at the same price.
GET /api/v1/stems/{job_id}
202 while processing;
200 with a
download_url per stem once done;
410 once the retrieval window has passed.
GET /api/v1/stems/{job_id}/download/{stem_name}
Streams one stem. No API key required — the unguessable job id is the
capability, so the URL works directly in a player or a browser.
Stem names are vocals, drums, bass,
other, or vocals and instrumental.
DELETE /api/v1/stems/{job_id}
Cancels a job still queued and releases its hold. A job already processing cannot be cancelled.
Karaoke video
Render an alignment you already have into a video. It takes the audio and the timings — not lyrics — so three formats of one song cost one alignment and three renders, not three of each.
POST /api/v1/video
| Field | Values | Notes |
|---|---|---|
audio | audio file | the track the video carries |
alignment | JSON | what GET /api/v1/sync/{job_id} returned. Either shape is accepted. |
orientation | landscape, portrait, square | 16:9, 9:16 or 1:1 |
resolution | 720p, 1080p, 4k | defaults to 1080p. Sets the price. |
render_mode | burned, greenscreen, alpha, cdg | MP4, MP4 on a key colour, ProRes MOV with transparency, or an MP3+G bundle. Sets the price. |
template | a name from the list below | defaults to classic |
style | JSON object | override any individual look parameter |
background | PNG, JPEG or WebP | your own still image behind the lyrics |
duet_map | {"a": [0,2], "b": [1,3]} | which singer sings which line, by index. Free — it changes colours, not processing. |
callback_url | HTTPS URL | required. The render is delivered here, not polled. |
webhook_secret | 16+ characters | signs the delivery, same scheme as every other webhook |
Answers 202 with a job_id.
The alignment must carry a positive audio_duration_s — it is
what the render is billed on — and every word timestamp must fall inside it.
GET /api/v1/video/{job_id}/download
Streams the finished file. No API key required — the unguessable job id is the capability, so the URL plays directly in a browser or a player. Available for 24 hours. A transparent render is a ProRes MOV, which browsers do not decode: download it and open it in your editor.
GET /api/v1/video/templates
The look library, as data you can build a picker from: name, label, description and
whether it renders today. A template marked available: false carries
available_when naming what it still needs — asking for one is a
400, never a silent fallback to a different look.
POST /api/v1/video/rerender
Renders from a completed /sync job by id, without paying for the alignment
again — pass the audio plus job_id and the timings are fetched for you.
Every parameter above applies. Billed at the video rate only.
Webhooks
If you pass a callback_url, we POST a JSON body to it when the job reaches a
terminal state. Delivery is asynchronous and applies to both hot and batch jobs with a single, identical signing scheme.
Delivery guarantees
- At-least-once delivery. A webhook may be delivered more than once. Deduplicate on
job_id— treat a repeat of ajob_idyou have already processed as a no-op. - Failed deliveries are retried with exponential backoff. Respond with a
2xxto acknowledge.
Verifying the signature
Every delivery carries an X-Webhook-Signature header. It is an
HMAC-SHA256 hex digest computed with your webhook signing secret
over the canonical JSON body — the payload serialized with
keys sorted alphabetically, UTF-8 encoded. Recompute it on your side and compare
(use a constant-time comparison). Reject the delivery if it does not match.
import hmac, hashlib, json
def verify(body_dict, signing_secret, received_signature):
canonical = json.dumps(body_dict, ensure_ascii=False, sort_keys=True)
expected = hmac.new(
signing_secret.encode("utf-8"),
canonical.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, received_signature)
Payload (completed)
{
"job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "completed",
"result_url": "https://tonael.com/api/v1/jobs/a1b2c3d4-.../result",
"expires_at": "2026-04-05T10:30:12Z",
"result": { "...": "inline alignment result" }
}
GET /api/v1/health
Health check endpoint. No authentication required. Returns the status of all system components.
Response 200 OK (all systems operational)
{
"status": "healthy",
"checks": {
"api": "ok",
"database": "ok",
"processing": "ok"
}
}
Response 503 Service Unavailable (degraded)
{
"status": "degraded",
"checks": {
"api": "ok",
"database": "ok",
"processing": "down"
}
}
Use this endpoint for uptime monitoring. Status healthy = all systems go. degraded = partial outage. unhealthy = critical failure.
Error Codes
| HTTP | Code | Description |
|---|---|---|
| 400 | INVALID_AUDIO | Unsupported, empty or undecodable audio — or neither/both of audio and audio_url were provided |
| 400 | AUDIO_TOO_LARGE | File exceeds 1 GB |
| 400 | INVALID_AUDIO_URL | audio_url is not an HTTPS URL, or its host is not publicly reachable |
| 400 | AUDIO_URL_UNREACHABLE | We could not download the audio at audio_url (timeout, non-200, or empty response) |
| 400 | AUDIO_TOO_LONG | Audio exceeds 3 hours |
| 400 | INVALID_LYRICS | Lyrics exceed 240,000 chars |
| 422 | LYRICS_REQUIRED | No lyrics provided — this endpoint aligns the lyrics you provide with your audio |
| 400 | UNSUPPORTED_LANGUAGE | Language code not supported |
| 401 | UNAUTHORIZED | Missing or invalid API key |
| 402 | INSUFFICIENT_CREDIT | Not enough credit for this job — the message shows available, required, and the exact shortfall in dollars. Top up via POST /api/v1/credits/topup (minimum $15) |
| 403 | KEY_PAUSED | API key is paused by administrator |
| 403 | KEY_REVOKED | API key has been revoked |
| 403 | BATCH_REQUIRES_PAID | Batch priority requires a paid account — add credit with your first top-up to unlock it (POST /api/v1/credits/topup) |
| 403 | WALLET_REQUIRED | This API key has no wallet and cannot be billed |
| 400 | INVALID_PRIORITY | The priority value is invalid — valid values are hot and batch |
| 400 | INVALID_BATCH_FALLBACK | The batch_fallback value is invalid — valid values are reject and hot |
| — | BATCH_WINDOW_ELAPSED | Terminal job state (webhook / status polling, not an HTTP error): the batch job was not processed within the 24-hour batch window and was fully refunded — resubmit with priority=hot, or use batch_fallback=hot to switch automatically next time |
| 404 | JOB_NOT_FOUND | Job ID does not exist or belongs to another key |
| 429 | CONCURRENCY_LIMIT | Too many jobs in flight for this key — retry once an in-flight job finishes |
| 409 | LEGACY_TOPUP_UNSUPPORTED | This account is dollar-denominated — use POST /api/v1/credits/topup with a pack (pack_15, pack_50, pack_200) or a custom amount_usd (minimum $15) |
| 409 | ACCOUNT_NOT_MIGRATED | This account is not yet dollar-denominated — the /api/v1/credits endpoints become available after migration |
| 400 | IDEMPOTENCY_KEY_REQUIRED | A top-up was sent without an Idempotency-Key header — it is required on POST /api/v1/credits/topup so a retried request can never charge your card twice |
| 409 | DUPLICATE_SUBMISSION | This Idempotency-Key was already used for a top-up — the original purchase stands and nothing was charged again. Use a new key for a genuinely new purchase |
| 422 | TAX_IDENTITY_REQUIRED | Set your billing details first via POST /api/v1/credits/tax-identity (country + business capacity) — they determine the VAT on your invoice |
| 410 | RESULT_EXPIRED | Result deleted after its retention window (24 hours for hot jobs, 72 hours for batch) |
| 503 | BATCH_UNAVAILABLE | Batch processing is temporarily unavailable — resubmit with priority=hot to process in the real-time lane |
| 500 | INTERNAL_ERROR | Unexpected server error |
All errors follow the format: {"error": {"code": "...", "message": "..."}}
Billing & Refunds
Failed jobs are never charged. When a job fails or is cancelled, the credit reserved for it is automatically returned to your balance — no action needed on your side. The recredit appears in GET /api/v1/billing/usage as a positive entry labeled “Job failed or cancelled (credit returned to balance)”.
Every job is billed individually. When you submit multiple jobs, each one is charged on its own: a failed job is recredited on its own, and never affects what you pay for the jobs that completed.
Credits are non-refundable. Prepaid credits are sold to professional customers and are not reimbursed in cash, whether consumed or not. Any commercial gesture is granted in credits. See the Terms of Sale for the full doctrine, including the statutory carve-out that applies if you purchase as a consumer.
Credits never expire. Your balance stays usable for as long as your account is open and in good standing. There is no validity period and no expiry sweep — on any account type, with or without a VAT ID.
Lyrics Guidelines
What you should do
Send the lyrics as-is: Just paste the raw lyrics text. The API handles everything else — formatting, cleaning, and segmentation are fully automatic.
Tip: For best results, include only what is actually sung or spoken. But even if you include metadata or headers, the API will do its best.
What the API handles automatically
AUTO Line segmentation: Long lines are automatically split at natural points (punctuation, pauses) for optimal karaoke display. You can send a single block of text — the API will segment it intelligently.
AUTO
Bracket tags: Section markers like [Verse 1], [Chorus], [SFX], [Female] are automatically stripped.
AUTO
Numbers: Digits are converted to words (10 → dix, 5 → cinq) based on the language parameter.
AUTO
Accented characters: Characters like é, à, ç, ö, ü are normalized internally. Original text is preserved in the output.
AUTO
Punctuation: Standalone punctuation (!, ?, :) is handled gracefully.
Limits
| Max audio duration | 3 hours |
| Max file size | 1 GB |
| Accepted formats | MP3, WAV, FLAC, M4A/AAC, OGG/Opus, AIFF, WMA, WebM, AMR, CAF, AU, WavPack, APE, TTA |
| Max lyrics length | 240,000 characters |
| Result retention | 24 hours (hot) · 72 hours (batch) |
| Processing time | ~10-15 seconds (typical 3-4 min song) |
| Concurrent jobs | 2 before a card is registered, 5 after — counted across alignment, separation and video together. Over the limit answers 429 CONCURRENCY_LIMIT; wait for one to finish or ask us for more. |
| Server at capacity | 503 SERVER_BUSY with a Retry-After header, when more uploads arrive than we can hold at once. Not your request’s fault — retry it. |