Tonael v1
On this page

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.

Base URL: 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%)
tonael
word-level alignment
$0.045 / min $0.028 / min
tonael_ultra
vocal 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 add priority=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_url to 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

FieldValuesNotes
audioMP3 or WAVthe file to separate
stems_modefull, vocal_instrumentalfour tracks, or vocals + instrumental. Sets the price.
output_formatmp3, wavMP3 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

FieldValuesNotes
audioaudio filethe track the video carries
alignmentJSONwhat GET /api/v1/sync/{job_id} returned. Either shape is accepted.
orientationlandscape, portrait, square16:9, 9:16 or 1:1
resolution720p, 1080p, 4kdefaults to 1080p. Sets the price.
render_modeburned, greenscreen, alpha, cdgMP4, MP4 on a key colour, ProRes MOV with transparency, or an MP3+G bundle. Sets the price.
templatea name from the list belowdefaults to classic
styleJSON objectoverride any individual look parameter
backgroundPNG, JPEG or WebPyour 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_urlHTTPS URLrequired. The render is delivered here, not polled.
webhook_secret16+ characterssigns 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 a job_id you have already processed as a no-op.
  • Failed deliveries are retried with exponential backoff. Respond with a 2xx to 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
400INVALID_AUDIOUnsupported, empty or undecodable audio — or neither/both of audio and audio_url were provided
400AUDIO_TOO_LARGEFile exceeds 1 GB
400INVALID_AUDIO_URLaudio_url is not an HTTPS URL, or its host is not publicly reachable
400AUDIO_URL_UNREACHABLEWe could not download the audio at audio_url (timeout, non-200, or empty response)
400AUDIO_TOO_LONGAudio exceeds 3 hours
400INVALID_LYRICSLyrics exceed 240,000 chars
422LYRICS_REQUIREDNo lyrics provided — this endpoint aligns the lyrics you provide with your audio
400UNSUPPORTED_LANGUAGELanguage code not supported
401UNAUTHORIZEDMissing or invalid API key
402INSUFFICIENT_CREDITNot 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)
403KEY_PAUSEDAPI key is paused by administrator
403KEY_REVOKEDAPI key has been revoked
403BATCH_REQUIRES_PAIDBatch priority requires a paid account — add credit with your first top-up to unlock it (POST /api/v1/credits/topup)
403WALLET_REQUIREDThis API key has no wallet and cannot be billed
400INVALID_PRIORITYThe priority value is invalid — valid values are hot and batch
400INVALID_BATCH_FALLBACKThe batch_fallback value is invalid — valid values are reject and hot
BATCH_WINDOW_ELAPSEDTerminal 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
404JOB_NOT_FOUNDJob ID does not exist or belongs to another key
429CONCURRENCY_LIMITToo many jobs in flight for this key — retry once an in-flight job finishes
409LEGACY_TOPUP_UNSUPPORTEDThis 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)
409ACCOUNT_NOT_MIGRATEDThis account is not yet dollar-denominated — the /api/v1/credits endpoints become available after migration
400IDEMPOTENCY_KEY_REQUIREDA 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
409DUPLICATE_SUBMISSIONThis 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
422TAX_IDENTITY_REQUIREDSet your billing details first via POST /api/v1/credits/tax-identity (country + business capacity) — they determine the VAT on your invoice
410RESULT_EXPIREDResult deleted after its retention window (24 hours for hot jobs, 72 hours for batch)
503BATCH_UNAVAILABLEBatch processing is temporarily unavailable — resubmit with priority=hot to process in the real-time lane
500INTERNAL_ERRORUnexpected 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 (10dix, 5cinq) 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 duration3 hours
Max file size1 GB
Accepted formatsMP3, WAV, FLAC, M4A/AAC, OGG/Opus, AIFF, WMA, WebM, AMR, CAF, AU, WavPack, APE, TTA
Max lyrics length240,000 characters
Result retention24 hours (hot) · 72 hours (batch)
Processing time~10-15 seconds (typical 3-4 min song)
Concurrent jobs2 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 capacity503 SERVER_BUSY with a Retry-After header, when more uploads arrive than we can hold at once. Not your request’s fault — retry it.