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.

Get a key in the studio

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.

Header
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, JSON
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, multipart
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.

Response
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
curl https://valiha.marindrano.com/v1/songs/cm3k2x8f40001 \
  -H "Authorization: Bearer $VALIHA_KEY"
curl
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
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
curl https://valiha.marindrano.com/v1/songs/cm3k2x8f40001/stems/vocals \
  -H "Authorization: Bearer $VALIHA_KEY" \
  -o vocals.m4a

Every endpoint

MethodPathWhat it does
POST/v1/songsCreate 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}/chordproThe 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}/analyzeAnalyze 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.

Request
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.

JavaScript, Node
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.

PublicSong
{
  "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.

Response
HTTP/1.1 404 Not Found

{ "error": { "code": "not_found", "message": "This song does not exist" } }
CodeStatusMeaning
unauthorized401The key is missing, wrong or revoked.
not_found404No such song in your organization.
invalid400A field is missing or wrong. The message says which.
limit429Too many requests, or a limit of your plan.
server500Something 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.