Developers
The Valiha API
Send a recording and its lyrics from your own app. Valiha sends back the key, the tempo, the sections, the chords on every beat, a ChordPro chart, the separated parts and the melody.
For organizations on the Plus plan or a partner plan. Owners and admins create keys under Organization, API.
Base URLhttps://valiha.marindrano.com/v1
Examples in JavaScript and Python, and every data format, are in the docs. Read the API guide
Authentication
Every request carries an API key in the Authorization header, and runs in the organization that owns the key.
Authorization: Bearer vk_live_...The full key is shown once, when it is created. Keep it on your server, never in an app or a web page. A revoked key stops working at once.
Create a song
Send the title, the language of the lyrics (mg, fr or en), the lyrics and the recording. Valiha answers at once with the new song and starts the analysis.
With a link to the recording
Valiha downloads the file itself: https only, up to 80 MB, within 60 seconds.
curl -X POST https://valiha.marindrano.com/v1/songs \
-H "Authorization: Bearer $VALIHA_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Antsoantso",
"artist": "Rija",
"language": "mg",
"lyrics": "Tompo ô, Ianao no hirako\nNy fitiavanao no tanjako",
"audioUrl": "https://example.com/antsoantso.mp3",
"webhookUrl": "https://example.com/hooks/valiha",
"externalId": "song-1042",
"options": { "stems": 6, "melody": ["game", "choir"] }
}'With the file
Send a multipart form: the recording in a file field named audio, the other fields beside it.
curl -X POST https://valiha.marindrano.com/v1/songs \
-H "Authorization: Bearer $VALIHA_KEY" \
-F "title=Antsoantso" \
-F "language=mg" \
-F "lyrics=<lyrics.txt" \
-F "audio=@antsoantso.mp3" \
-F "webhookUrl=https://example.com/hooks/valiha" \
-F "externalId=song-1042" \
-F 'options={"stems": 6, "melody": ["game", "choir"]}'webhookUrl and externalId are optional. externalId is your own id for the song, to find it again later.
Parts and notes
Add options to choose what the analysis makes. stems is 2, 4 or 6 parts: 2 gives vocals and music, 4 gives vocals, drums, bass and other, 6 adds guitar and piano. melody lists the note models: game writes the main melody, choir writes every voice it hears (experimental). Without options, Valiha makes 4 parts and the main melody.
In a multipart form, send options as one field that holds the same JSON.
HTTP/1.1 202 Accepted
{
"id": "cm3k2x8f40001",
"externalId": "song-1042",
"status": "queued",
"options": { "stems": 6, "melody": ["game", "choir"] },
"createdAt": "2026-10-27T09:12:00.000Z"
}Get the chart
An analysis takes a few minutes. Ask for the song now and then, or give a webhookUrl and Valiha calls you when it is done.
Ask for it
Ask every 10 seconds or so until the status is ready or failed. You can also find your songs by their externalId.
curl https://valiha.marindrano.com/v1/songs/cm3k2x8f40001 \
-H "Authorization: Bearer $VALIHA_KEY"curl "https://valiha.marindrano.com/v1/songs?externalId=song-1042" \
-H "Authorization: Bearer $VALIHA_KEY"Analyze again
Send options to change the parts or the notes, and they become the song's. Without a body, the song keeps its options. The chart you have stays until the new one is ready.
curl -X POST https://valiha.marindrano.com/v1/songs/cm3k2x8f40001/analyze \
-H "Authorization: Bearer $VALIHA_KEY" \
-H "Content-Type: application/json" \
-d '{ "options": { "stems": 2, "melody": ["game"] } }'Download a part
stems lists the parts of the current analysis. Download each one with your key. A part is AAC audio in an .m4a file, and Range requests work.
curl https://valiha.marindrano.com/v1/songs/cm3k2x8f40001/stems/vocals \
-H "Authorization: Bearer $VALIHA_KEY" \
-o vocals.m4aEvery endpoint
| Method | Path | What it does |
|---|---|---|
| POST | /v1/songs | Create a song and start its analysis. Answers 202. |
| GET | /v1/songs/{id} | One song, in the shape below. |
| GET | /v1/songs?externalId=... | Your songs with this externalId, newest first. |
| GET | /v1/songs/{id}/chordpro | The chart as ChordPro text. 404 until the song is ready. |
| GET | /v1/songs/{id}/stems/{name} | One part of the current analysis, as AAC audio (.m4a). 404 when the song has no such part. |
| POST | /v1/songs/{id}/analyze | Analyze the song again, with new options if you send them. Answers 202. |
| DELETE | /v1/songs/{id} | Delete the song and its recording. Answers 204. |
Webhooks
When an analysis ends, Valiha posts the song to its webhookUrl, with the event song.ready or song.failed.
POST https://example.com/hooks/valiha
X-Valiha-Event: song.ready
X-Valiha-Timestamp: 1793092532
X-Valiha-Signature: sha256=5f0c9a...
Content-Type: application/json
{ "event": "song.ready", "song": { "id": "cm3k2x8f40001", "status": "ready", ... } }Answer with any 2xx within 10 seconds. If not, Valiha tries again after 30 seconds, then after 5 minutes, then stops.
Check the signature
Each key comes with a webhook secret, shown once with the key. The signature is an HMAC SHA-256 of the timestamp, a dot and the raw body, made with that secret. Compare it in constant time, and refuse timestamps older than five minutes.
import { createHmac, timingSafeEqual } from "node:crypto";
export function isFromValiha(rawBody, headers, webhookSecret) {
const timestamp = headers["x-valiha-timestamp"];
const signature = headers["x-valiha-signature"] ?? "";
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!timestamp || !(age < 300)) return false;
const expected =
"sha256=" +
createHmac("sha256", webhookSecret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}The song object
Every endpoint and every webhook gives songs in this shape. chart stays null until the song is ready. Its sections and chords carry times in seconds, so you can place the chords without reading the ChordPro text.
{
"id": "cm3k2x8f40001",
"externalId": "song-1042",
"status": "ready",
"title": "Antsoantso",
"artist": "Rija",
"language": "mg",
"key": "G",
"bpm": 91,
"timeSignature": "4/4",
"durationSeconds": 212.4,
"error": null,
"chart": {
"chordpro": "{title: Antsoantso}\n{key: G}\n...",
"sections": [
{ "kind": "verse", "label": "Andininy 1", "start": 8.2, "end": 41.6 }
],
"chords": [
{ "time": 8.21, "beat": 12, "name": "G" },
{ "time": 10.84, "beat": 16, "name": "D" }
]
},
"options": { "stems": 6, "melody": ["game", "choir"] },
"stems": ["vocals", "drums", "bass", "guitar", "piano", "other"],
"melody": {
"version": 1,
"model": "game",
"engine": { "version": "0.1.0" },
"sha1": "9b1c4e...",
"tuning": -12.5,
"notes": [
{ "start": 8.31, "end": 8.74, "midi": 67, "beat": 12, "length": 1, "conf": 1, "syllable": [0, 0, 0, 0] }
],
"timings": { "notes": 0.41 }
},
"voices": {
"version": 1,
"model": "choir",
"sha1": "9b1c4e...",
"tuning": -12.5,
"notes": [
{ "start": 8.3, "end": 8.76, "midi": 67, "velocity": 0.81 },
{ "start": 8.3, "end": 8.76, "midi": 59, "velocity": 0.44 }
],
"timings": { "features": 3.2, "model": 18.7 }
},
"createdAt": "2026-10-27T09:12:00.000Z",
"updatedAt": "2026-10-27T09:15:32.000Z"
}options says what the analysis is asked to make. stems, melody and voices come from the current analysis: the parts you can download, the main melody (melody.json) and every voice (voices.json). melody and voices are null when the analysis has none.
status is queued, analyzing, ready or failed. When it is failed, error says why.
Errors
Errors come back as JSON, with a code for your program and a message for people.
HTTP/1.1 404 Not Found
{ "error": { "code": "not_found", "message": "This song does not exist" } }| Code | Status | Meaning |
|---|---|---|
| unauthorized | 401 | The key is missing, wrong or revoked. |
| not_found | 404 | No such song in your organization. |
| invalid | 400 | A field is missing or wrong. The message says which. |
| limit | 429 | Too many requests, or a limit of your plan. |
| server | 500 | Something failed on our side. Try again a little later. |
Rate limit
Each key can make 60 requests a minute. Past that, Valiha answers 429 with a Retry-After header, in seconds.