curl --request POST \
--url https://api.audivo.dev/v1/uploads \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"sha256": "3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c",
"bytes": 28934112,
"content_type": "audio/mpeg",
"declared_duration_seconds": 1807.4,
"title": "Interview take two"
}
'import requests
url = "https://api.audivo.dev/v1/uploads"
payload = {
"sha256": "3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c",
"bytes": 28934112,
"content_type": "audio/mpeg",
"declared_duration_seconds": 1807.4,
"title": "Interview take two"
}
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({
sha256: '3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c',
bytes: 28934112,
content_type: 'audio/mpeg',
declared_duration_seconds: 1807.4,
title: 'Interview take two'
})
};
fetch('https://api.audivo.dev/v1/uploads', 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://api.audivo.dev/v1/uploads",
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([
'sha256' => '3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c',
'bytes' => 28934112,
'content_type' => 'audio/mpeg',
'declared_duration_seconds' => 1807.4,
'title' => 'Interview take two'
]),
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://api.audivo.dev/v1/uploads"
payload := strings.NewReader("{\n \"sha256\": \"3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c\",\n \"bytes\": 28934112,\n \"content_type\": \"audio/mpeg\",\n \"declared_duration_seconds\": 1807.4,\n \"title\": \"Interview take two\"\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://api.audivo.dev/v1/uploads")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"sha256\": \"3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c\",\n \"bytes\": 28934112,\n \"content_type\": \"audio/mpeg\",\n \"declared_duration_seconds\": 1807.4,\n \"title\": \"Interview take two\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.audivo.dev/v1/uploads")
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 \"sha256\": \"3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c\",\n \"bytes\": 28934112,\n \"content_type\": \"audio/mpeg\",\n \"declared_duration_seconds\": 1807.4,\n \"title\": \"Interview take two\"\n}"
response = http.request(request)
puts response.read_body{
"upload_id": "upl_7c1f0a9b3e2d4c5b6a7f8e9d",
"put_url": "https://hark-uploads-prod-061039778683.s3.us-east-1.amazonaws.com/acct_v3tptvk6pnf5cdiq/upl_7c1f0a9b3e2d4c5b6a7f8e9d?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=3600&X-Amz-SignedHeaders=content-length%3Bcontent-type%3Bhost%3Bx-amz-checksum-sha256&X-Amz-Signature=0123456789abcdef",
"put_headers": {
"content-type": "audio/mpeg",
"content-length": "28934112",
"x-amz-checksum-sha256": "P4qcKx1OnywdTp88HU6fLD+KnCsdTp8sHU6fLB1Onyw="
},
"put_url_expires_at": "2026-09-17T11:00:00Z",
"retained_until": "2026-09-24T10:00:00Z",
"bytes": 28934112,
"content_type": "audio/mpeg",
"declared_duration_seconds": 1807.4,
"title": "Interview take two"
}{
"error": {
"code": "invalid_request",
"type": "invalid_request",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"retryable": true
}
}{
"error": {
"code": "invalid_request",
"type": "invalid_request",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"retryable": true
}
}{
"error": {
"code": "invalid_request",
"type": "invalid_request",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"retryable": true
}
}{
"error": {
"type": "rate_limited",
"code": "upload_quota_exceeded",
"message": "This account already holds 100 unexpired uploads totalling 10 GiB, the account's allowance; enough capacity returns at 2026-09-24T10:00:00Z, when the oldest upload's reservation releases.",
"doc_url": "https://docs.audivo.dev/errors#upload_quota_exceeded",
"request_id": "req_3c8f1a9d2b4e",
"retryable": true
}
}{
"error": {
"code": "invalid_request",
"type": "invalid_request",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"retryable": true
}
}Announce a file to upload
Records what the caller knows about an audio file and returns a presigned PUT for it. The URL is signed against the declared SHA-256, so S3 refuses a body whose hash differs; the API never reads the bytes. Send the file with exactly the put_headers returned. Then name it in a quote as uploads: [{ upload_id }].
The upload is private to the account: its transcript is cached for this account alone. The upload becomes unavailable at retained_until; a quote after that reports upload_not_found. Physical object and record cleanup is asynchronous. Each account may announce at most 10 GiB across 100 unexpired uploads. Announcements reserve this allowance even if no file is sent; capacity returns at their logical expiry. A refusal returns 429 upload_quota_exceeded before a PUT URL is issued.
curl --request POST \
--url https://api.audivo.dev/v1/uploads \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"sha256": "3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c",
"bytes": 28934112,
"content_type": "audio/mpeg",
"declared_duration_seconds": 1807.4,
"title": "Interview take two"
}
'import requests
url = "https://api.audivo.dev/v1/uploads"
payload = {
"sha256": "3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c",
"bytes": 28934112,
"content_type": "audio/mpeg",
"declared_duration_seconds": 1807.4,
"title": "Interview take two"
}
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({
sha256: '3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c',
bytes: 28934112,
content_type: 'audio/mpeg',
declared_duration_seconds: 1807.4,
title: 'Interview take two'
})
};
fetch('https://api.audivo.dev/v1/uploads', 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://api.audivo.dev/v1/uploads",
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([
'sha256' => '3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c',
'bytes' => 28934112,
'content_type' => 'audio/mpeg',
'declared_duration_seconds' => 1807.4,
'title' => 'Interview take two'
]),
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://api.audivo.dev/v1/uploads"
payload := strings.NewReader("{\n \"sha256\": \"3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c\",\n \"bytes\": 28934112,\n \"content_type\": \"audio/mpeg\",\n \"declared_duration_seconds\": 1807.4,\n \"title\": \"Interview take two\"\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://api.audivo.dev/v1/uploads")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"sha256\": \"3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c\",\n \"bytes\": 28934112,\n \"content_type\": \"audio/mpeg\",\n \"declared_duration_seconds\": 1807.4,\n \"title\": \"Interview take two\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.audivo.dev/v1/uploads")
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 \"sha256\": \"3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c\",\n \"bytes\": 28934112,\n \"content_type\": \"audio/mpeg\",\n \"declared_duration_seconds\": 1807.4,\n \"title\": \"Interview take two\"\n}"
response = http.request(request)
puts response.read_body{
"upload_id": "upl_7c1f0a9b3e2d4c5b6a7f8e9d",
"put_url": "https://hark-uploads-prod-061039778683.s3.us-east-1.amazonaws.com/acct_v3tptvk6pnf5cdiq/upl_7c1f0a9b3e2d4c5b6a7f8e9d?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=3600&X-Amz-SignedHeaders=content-length%3Bcontent-type%3Bhost%3Bx-amz-checksum-sha256&X-Amz-Signature=0123456789abcdef",
"put_headers": {
"content-type": "audio/mpeg",
"content-length": "28934112",
"x-amz-checksum-sha256": "P4qcKx1OnywdTp88HU6fLD+KnCsdTp8sHU6fLB1Onyw="
},
"put_url_expires_at": "2026-09-17T11:00:00Z",
"retained_until": "2026-09-24T10:00:00Z",
"bytes": 28934112,
"content_type": "audio/mpeg",
"declared_duration_seconds": 1807.4,
"title": "Interview take two"
}{
"error": {
"code": "invalid_request",
"type": "invalid_request",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"retryable": true
}
}{
"error": {
"code": "invalid_request",
"type": "invalid_request",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"retryable": true
}
}{
"error": {
"code": "invalid_request",
"type": "invalid_request",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"retryable": true
}
}{
"error": {
"type": "rate_limited",
"code": "upload_quota_exceeded",
"message": "This account already holds 100 unexpired uploads totalling 10 GiB, the account's allowance; enough capacity returns at 2026-09-24T10:00:00Z, when the oldest upload's reservation releases.",
"doc_url": "https://docs.audivo.dev/errors#upload_quota_exceeded",
"request_id": "req_3c8f1a9d2b4e",
"retryable": true
}
}{
"error": {
"code": "invalid_request",
"type": "invalid_request",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"retryable": true
}
}Authorizations
Authorization: Bearer hk_live_... for live keys or Authorization: Bearer hk_test_... for test-mode keys. hk_test_ keys resolve real public catalog metadata but return deterministic committed fixtures, never call inference, and never mutate live credits. This is the only transport for the credential: the x-api-key alias once documented was removed in 0.2.0, because the edge authorizer reads Authorization as its single identity source and a request on any other header is refused before it is authenticated.
Never accepted on a dashboardJwt operation, and there are no exceptions. GET /v1/usage and GET /v1/limits briefly declared both schemes (0.8.0); that was withdrawn in 0.8.1 because no deployed route could honor it — both operations are served by the control-plane API, whose authorizer verifies a Cognito token and refuses an hk_live_ credential on shape, and the customer API does not route either path. An API-key holder reads its balance and reservation from QuoteResponse, which carries balance_credits and reserved_credits on every quote. Every operation in this document takes one scheme or the other and refuses the wrong one as unauthenticated.
Body
The file's SHA-256, lowercase hex. The PUT is signed against it.
^[a-f0-9]{64}$The file's exact length; the PUT must carry the same Content-Length.
1 <= x <= 5368709120audio/mpeg, audio/mp3, audio/mp4, audio/m4a, audio/x-m4a, audio/aac, audio/x-aac, audio/ogg, audio/opus, audio/flac, audio/x-flac, audio/wav, audio/x-wav, audio/webm What the quote is priced from. A job whose audio runs past the ceiling this reserves fails as declared_duration_exceeded and releases the reservation.
x <= 36000How the entry is labelled; the private show is always "Uploads".
1 - 300Response
The upload is recorded; PUT the file next.
Opaque, server-generated upload identifier.
40^upl_[A-Za-z0-9]{16,32}$Presigned PUT; send the file body with put_headers and nothing else.
4096Every header the PUT must carry, verbatim; they are part of the signature.
Show child attributes
Show child attributes
Logical upload expiry and allowance-release time; physical cleanup is asynchronous.
x >= 1audio/mpeg, audio/mp3, audio/mp4, audio/m4a, audio/x-m4a, audio/aac, audio/x-aac, audio/ogg, audio/opus, audio/flac, audio/x-flac, audio/wav, audio/x-wav, audio/webm 300
