Get started
Run your first job
AudioKit works in jobs. You send a link to a file and the operation you want; AudioKit checks the file, runs the operation and sends the result to your webhook.
Before you start
- An API key. AudioKit is in early access: request a key. Keys look like
ak_…. - A file AudioKit can download. An https URL to it, usually a signed GET URL from your storage. It must resolve to a public address, including after redirects.
- The file's sha256. Hash the exact bytes your URL serves, for example with
sha256sum master.wav. - Optional: a webhook endpoint. An https URL that accepts POST. Without one, you poll for the result.
Check the service
These two endpoints need no key. /v1/capabilities lists the operations the server runs right now.
curl https://api-production-cd43.up.railway.app/v1/health curl https://api-production-cd43.up.railway.app/v1/capabilities
{ "status": "ok", "version": "0.1.0" }
{ "operations": [
{ "operation": "loudness", "outputs": [], "async": true, "requires_consent": false },
{ "operation": "preview", "outputs": ["audio"], "async": true, "requires_consent": false },
…
] }
Submit a job
This measures a file's loudness. job_id is yours: use something you can look up later. It's also the idempotency key.
curl https://api-production-cd43.up.railway.app/v1/jobs \
-H "Authorization: Bearer $AUDIOKIT_KEY" \
-H "Content-Type: application/json" \
-d '{
"job_id": "job-0193",
"operation": "loudness",
"input": {
"url": "https://storage.example.com/master.wav?signature=abc",
"sha256": "837d2e9c0f1a4b5c6d7e8f9012a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5",
"filename": "master.wav"
},
"webhook": { "url": "https://you.example/audiokit/webhook" }
}'
You get 202 Accepted and the job, with status: "queued" and AudioKit's own provider_job_id. Keep that ID: it's how you fetch or cancel the job.
Other operations work the same way. Some take params; the reference lists them for each operation.
Get the result
When the job finishes, AudioKit POSTs a job.succeeded, job.failed or job.canceled event to your webhook, with the job as data. You can also fetch the job at any time:
curl https://api-production-cd43.up.railway.app/v1/jobs/$PROVIDER_JOB_ID \ -H "Authorization: Bearer $AUDIOKIT_KEY"
{
"provider_job_id": "01a19981-fb9f-774e-b139-7cc6ac648659",
"job_id": "job-0193",
"operation": "loudness",
"status": "succeeded",
"result": {
"integrated_lufs": -22.02,
"loudness_range_lu": 0.0,
"true_peak_dbtp": -21.33,
"sample_peak_dbfs": -21.33,
"sample_rate": 44100,
"channels": 2,
"duration_seconds": 5.0
},
"provenance": {
"input_sha256": "837d2e9c0f1a4b5c6d7e8f9012a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5",
"audiokit_version": "0.1.0",
"operation": "loudness",
"params": {}
},
"created_at": "2026-11-01T23:46:35.039517Z",
"started_at": "2026-11-01T23:46:35.044170Z",
"finished_at": "2026-11-01T23:46:35.730192Z"
}
status moves from queued to running to succeeded, failed or canceled. A failed job has error: { "code", "message" } instead of result; the codes are in the reference.
Verify webhooks
Every event carries X-Signature: t=<unix seconds>,v1=<hex>, an HMAC-SHA256 of "{t}.{raw_body}" keyed with your webhook secret. Verify it against the raw body before parsing, and reject timestamps more than 300 seconds old.
import crypto from "node:crypto";
export function verify(secret, rawBody, header, now = Date.now() / 1000) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(now - t) > 300) return false;
const mac = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return mac.length === parts.v1?.length &&
crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(parts.v1));
}
import hashlib, hmac, time
def verify(secret: bytes, raw_body: bytes, header: str, now: float | None = None) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
t = int(parts.get("t", 0))
if not t or abs((now or time.time()) - t) > 300:
return False
mac = hmac.new(secret, f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, parts.get("v1", ""))
Check your code against this vector. Its timestamp is fixed, so pass now as 1793440800 when testing with it.
secret whsec_test
body {"event_id":"evt-1"}
header t=1793440800,v1=27d3e5b970e7b39136241354c673e7849a3bceccff29da0b09c76a1d9b73b822Reply with any 2xx to acknowledge. Otherwise AudioKit retries after 10 s, 1 min, 5 min, 30 min, 2 h and 6 h, then stops; the job itself always has the final state. Each event has a unique event_id: ignore ones you've already handled.
Retries and idempotency
If a submit times out, send the same request again. The same body with the same job_id returns the existing job with 200, so nothing runs twice. The same job_id with a different body is refused with 409 idempotency_conflict. To process a file again, use a new job_id.
Operations that write files
transcode and preview write a new audio file. Give AudioKit somewhere to put it: a signed PUT URL in outputs.audio, with any headers the signature needs. AudioKit sets Content-Type and Content-Length itself.
{
"job_id": "job-0194",
"operation": "preview",
"input": { "url": "https://storage.example.com/master.wav?signature=abc", "sha256": "837d2e9c…" },
"params": { "duration_seconds": 30 },
"outputs": {
"audio": {
"url": "https://storage.example.com/upload/preview.mp3?signature=def",
"headers": { "x-upsert": "true" }
}
},
"webhook": { "url": "https://you.example/audiokit/webhook" }
}
The result reports the uploaded file's sha256 and size_bytes. Hash what you stored and compare.
Cancel a job
POST /v1/jobs/{provider_job_id}/cancel. A queued job is canceled at once (200). A running job is asked to stop (202) and a job.canceled event follows. A finished job comes back unchanged (200).