curl --request POST \
--url https://cue.vibeset.ai/v1/beats \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"audio_url": "https://your-storage/track.mp3",
"track_id": "<string>",
"known_bpm": 128,
"audio_sha256": "<string>"
}
'import requests
url = "https://cue.vibeset.ai/v1/beats"
payload = {
"audio_url": "https://your-storage/track.mp3",
"track_id": "<string>",
"known_bpm": 128,
"audio_sha256": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
audio_url: 'https://your-storage/track.mp3',
track_id: '<string>',
known_bpm: 128,
audio_sha256: '<string>'
})
};
fetch('https://cue.vibeset.ai/v1/beats', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://cue.vibeset.ai/v1/beats",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'audio_url' => 'https://your-storage/track.mp3',
'track_id' => '<string>',
'known_bpm' => 128,
'audio_sha256' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://cue.vibeset.ai/v1/beats"
payload := strings.NewReader("{\n \"audio_url\": \"https://your-storage/track.mp3\",\n \"track_id\": \"<string>\",\n \"known_bpm\": 128,\n \"audio_sha256\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://cue.vibeset.ai/v1/beats")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"audio_url\": \"https://your-storage/track.mp3\",\n \"track_id\": \"<string>\",\n \"known_bpm\": 128,\n \"audio_sha256\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://cue.vibeset.ai/v1/beats")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"audio_url\": \"https://your-storage/track.mp3\",\n \"track_id\": \"<string>\",\n \"known_bpm\": 128,\n \"audio_sha256\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"job_id": "<string>",
"track_id": "<string>",
"status": "processing",
"poll": "/v1/beats/9f2c",
"next": "<string>",
"idempotent_replay": true
}{
"error": {
"type": "invalid_request",
"message": "Provide either 'video_id' or 'video_url'.",
"request_id": "a1b2c3d4e5f6"
}
}{
"error": {
"type": "invalid_request",
"message": "Provide either 'video_id' or 'video_url'.",
"request_id": "a1b2c3d4e5f6"
}
}{
"error": {
"type": "invalid_request",
"message": "Provide either 'video_id' or 'video_url'.",
"request_id": "a1b2c3d4e5f6"
}
}{
"error": {
"type": "invalid_request",
"message": "Provide either 'video_id' or 'video_url'.",
"request_id": "a1b2c3d4e5f6"
}
}{
"error": {
"type": "invalid_request",
"message": "Provide either 'video_id' or 'video_url'.",
"request_id": "a1b2c3d4e5f6"
}
}{
"error": {
"type": "invalid_request",
"message": "Provide either 'video_id' or 'video_url'.",
"request_id": "a1b2c3d4e5f6"
}
}Map a track's beats and downbeats
Map a track’s beat grid. Returns every beat, and separately every downbeat, the first beat of each bar, which is what decides whether a cut lands musically right.
Always asynchronous: mapping is real compute on real audio, so this returns 202 with
a job_id. Poll GET /v1/beats/{job_id}. Billed per minute of audio, once, on
success, and a failed job is not billed.
The submit checks only that audio_url is present and is an https:// or s3:// URL.
Everything about the audio itself is decided by the worker after it fetches the file,
and reaches you as a 422 on the poll: that it decodes, that it is under the
30-minute limit.
curl --request POST \
--url https://cue.vibeset.ai/v1/beats \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"audio_url": "https://your-storage/track.mp3",
"track_id": "<string>",
"known_bpm": 128,
"audio_sha256": "<string>"
}
'import requests
url = "https://cue.vibeset.ai/v1/beats"
payload = {
"audio_url": "https://your-storage/track.mp3",
"track_id": "<string>",
"known_bpm": 128,
"audio_sha256": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
audio_url: 'https://your-storage/track.mp3',
track_id: '<string>',
known_bpm: 128,
audio_sha256: '<string>'
})
};
fetch('https://cue.vibeset.ai/v1/beats', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://cue.vibeset.ai/v1/beats",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'audio_url' => 'https://your-storage/track.mp3',
'track_id' => '<string>',
'known_bpm' => 128,
'audio_sha256' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://cue.vibeset.ai/v1/beats"
payload := strings.NewReader("{\n \"audio_url\": \"https://your-storage/track.mp3\",\n \"track_id\": \"<string>\",\n \"known_bpm\": 128,\n \"audio_sha256\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://cue.vibeset.ai/v1/beats")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"audio_url\": \"https://your-storage/track.mp3\",\n \"track_id\": \"<string>\",\n \"known_bpm\": 128,\n \"audio_sha256\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://cue.vibeset.ai/v1/beats")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"audio_url\": \"https://your-storage/track.mp3\",\n \"track_id\": \"<string>\",\n \"known_bpm\": 128,\n \"audio_sha256\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"job_id": "<string>",
"track_id": "<string>",
"status": "processing",
"poll": "/v1/beats/9f2c",
"next": "<string>",
"idempotent_replay": true
}{
"error": {
"type": "invalid_request",
"message": "Provide either 'video_id' or 'video_url'.",
"request_id": "a1b2c3d4e5f6"
}
}{
"error": {
"type": "invalid_request",
"message": "Provide either 'video_id' or 'video_url'.",
"request_id": "a1b2c3d4e5f6"
}
}{
"error": {
"type": "invalid_request",
"message": "Provide either 'video_id' or 'video_url'.",
"request_id": "a1b2c3d4e5f6"
}
}{
"error": {
"type": "invalid_request",
"message": "Provide either 'video_id' or 'video_url'.",
"request_id": "a1b2c3d4e5f6"
}
}{
"error": {
"type": "invalid_request",
"message": "Provide either 'video_id' or 'video_url'.",
"request_id": "a1b2c3d4e5f6"
}
}{
"error": {
"type": "invalid_request",
"message": "Provide either 'video_id' or 'video_url'.",
"request_id": "a1b2c3d4e5f6"
}
}Authorizations
Your API key as a Bearer token, e.g. Authorization: Bearer vbsk_live_...
Headers
A true idempotency key, unlike the match-family endpoints. A repeat with the same
key returns the ORIGINAL job_id instead of starting a second job, so a timeout you
never saw the response to cannot bill you twice. Honoured for 24 hours. Reusing a
key for a different request is a 422.
Re-signing the URL between attempts is fine. The request is identified by the object it points at, not by the signature on it.
255Body
Signed https:// URL to the audio, from whatever storage you already use:
S3 presigned, GCS V4 signed, Azure SAS, R2/B2 presigned, or your own
CDN. Nothing has to be public. Must be reachable from the public internet
and respond within 120 seconds.
Sign for 12-24 hours when mapping a catalog: jobs queue, and a link signed for one hour can expire while its job is still waiting.
s3:// is accepted only for buckets outside our own account, and only once
you have granted our worker read access with a bucket policy.
2048"https://your-storage/track.mp3"
Your own identifier, echoed back on the job so you can map a catalog without keeping a job_id-to-track table of your own.
128Your catalog's tempo for this track, if you have one. Worth +6.73 downbeat CMLt on GTZAN when correct, and it tolerates being a few percent off (+6.65 at 5% error, +4.76 at 10%).
Only the OCTAVE matters: half or double the real tempo measured around -60, so we verify your value against our own unconstrained reading and discard it when the two disagree by an octave. Sending a bad tempo cannot make the result worse than omitting the field.
20 <= x <= 400128
SHA-256 of the audio bytes, if you already know it. When this account has mapped those exact bytes before, we answer from cache without fetching the file at all, which is what you want when re-running a catalog. Omitting it only costs the fetch, because we hash whatever we download regardless.
^[0-9a-fA-F]{64}$Response
Accepted. Poll the job. idempotent_replay: true means this key had already
started a job and nothing new was created or billed.