API
Ky, from your own code.
Send songs, stems and MIDI files, poll for the verdict, read what lined up. Plain HTTPS and JSON, a key in a header, no SDK.
Overview
The Ky API does what the site does. You create a check holding one or more files; each file becomes an item; an engine compares each item with the catalogue, or with your other files, for musical resemblance (melody and bass line, chords, rhythm, form and words) and gives it one of four verdicts, a one-line headline and a short explanation.
Every request goes to https://ky.ax. Requests and responses are JSON, apart from the files themselves. Times are Unix seconds, sizes are bytes.
Ky is a non-commercial research beta. A result describes musical resemblance; it is not a legal conclusion and not legal advice. Read the Results Disclaimer, and link it wherever you show a result.
curl https://ky.ax/api/v1/me \
-H "Authorization: Bearer $KY_API_KEY"
import os, requests
r = requests.get(
"https://ky.ax/api/v1/me",
headers={"Authorization": f"Bearer {os.environ['KY_API_KEY']}"},
)
print(r.json())
const r = await fetch("https://ky.ax/api/v1/me", {
headers: { Authorization: `Bearer ${process.env.KY_API_KEY}` },
});
console.log(await r.json());
{
"user": { "id": "u_…", "email": "you@example.com" },
"engine": { "online": true }
}
Authentication
Send an API key in the Authorization header of every request:
Authorization: Bearer ky_live_…
Keys
Create a key under Plugins & API keys in your account. A key is shown once, when you create it: store it somewhere safe. Ky keeps only a hash of it. Revoke a key there at any time and it stops working at once. Another account's check always answers 404.
What a key may do
A key has one of two scopes, chosen when it is made:
- Sends and reads its own checks (the default, and what a connected plugin gets): create checks, send files, and read the checks it made, with their results and evidence records.
- Full access (tick the box when you create the key, for your own code only): everything above for every check in the account, plus listing, downloading checked MIDI files, deleting, usage and billing.
A call outside a key's scope answers 403 scope. Give a plugin a full-access key and anyone who reads its settings file can read your account.
Device-code connect
For an app or plugin that other people install, don't ask them to paste a key. Start a connection, open the verification_url in their browser, and poll. They sign in (or create a free account), approve, and your next poll returns a key for them, exactly once. A key made this way sends files and reads the checks it made, and nothing else of their account.
POST /api/v1/device/startwith your app's name.- Show the
user_codeand openverification_url. POST /api/v1/device/polleveryintervalseconds untilstatusisapproved(ordenied,expired).
A code lasts ten minutes. A network can start 30 connections an hour.
curl -X POST https://ky.ax/api/v1/device/start \
-H "Content-Type: application/json" \
-d '{"client": "My App"}'
# open verification_url, then poll every 3 seconds:
curl -X POST https://ky.ax/api/v1/device/poll \
-H "Content-Type: application/json" \
-d '{"device_code": "…"}'
import time, webbrowser, requests
API = "https://ky.ax/api/v1"
start = requests.post(f"{API}/device/start", json={"client": "My App"}).json()
print("Your code:", start["user_code"])
webbrowser.open(start["verification_url"])
while True:
time.sleep(start["interval"])
poll = requests.post(f"{API}/device/poll", json={"device_code": start["device_code"]}).json()
if poll["status"] != "pending":
break
api_key = poll.get("api_key") # only when status is "approved"
const API = "https://ky.ax/api/v1";
const post = (path, body) =>
fetch(API + path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}).then((r) => r.json());
const start = await post("/device/start", { client: "My App" });
console.log("Open", start.verification_url, "and enter", start.user_code);
let poll;
do {
await new Promise((r) => setTimeout(r, start.interval * 1000));
poll = await post("/device/poll", { device_code: start.device_code });
} while (poll.status === "pending");
const apiKey = poll.api_key; // only when status is "approved"
{
"device_code": "N2c4…",
"user_code": "KY-7VZJ-3ZDB",
"verification_url": "https://ky.ax/connect?code=KY-7VZJ-3ZDB",
"interval": 3,
"expires_in": 600
}
{ "status": "approved", "api_key": "ky_live_…", "client": "My App" }
Quick start
Check a song end to end in three calls.
- Declare the file.
POST /api/v1/checkwith JSON naming the file and its size. You get the check'sidback, and anupload_urlfor the file. - Send it.
PUTthe file's bytes to theupload_url, with its exact size asContent-Length. A MIDI file can go in one multipart request instead (see Uploading). - Wait, then read the verdict.
GET /api/v1/checks/{id}every few seconds untilstatusisdoneorerror. Each item has averdict, aheadline, anexplanationand aurlto its result page; for every measure the engine took, downloadevidence_url.
Set KY_API_KEY in your environment first. The Python sample uses requests; the JavaScript runs in Node 18 or later.
# 1. declare the song
SIZE=$(wc -c < my-song.mp3 | tr -d ' ')
curl https://ky.ax/api/v1/check \
-H "Authorization: Bearer $KY_API_KEY" \
-H "X-Ky-Client: My App" \
-H "Content-Type: application/json" \
-d "{\"mode\": \"catalogue\", \"files\": [{\"name\": \"my-song.mp3\", \"size\": $SIZE}]}"
# 2. send it to the upload_url you got back
curl --upload-file my-song.mp3 \
"https://ky.ax/api/v1/checks/c_…/items/i_…/upload" \
-H "Authorization: Bearer $KY_API_KEY" \
-H "Content-Type: application/octet-stream"
# 3. poll until status is done, then read each item's verdict
curl "https://ky.ax/api/v1/checks/c_…?summary=0" \
-H "Authorization: Bearer $KY_API_KEY"
import os, time, requests
API = "https://ky.ax/api/v1"
H = {"Authorization": f"Bearer {os.environ['KY_API_KEY']}", "X-Ky-Client": "My App"}
path = "my-song.mp3"
size = os.path.getsize(path)
# 1. declare the song
r = requests.post(f"{API}/check", headers=H, json={
"mode": "catalogue",
"files": [{"name": path, "size": size}],
})
r.raise_for_status()
check = r.json()
# 2. send it, streamed from disk with its exact length
with open(path, "rb") as f:
requests.put("https://ky.ax" + check["items"][0]["upload_url"], data=f, headers={
**H, "Content-Type": "application/octet-stream", "Content-Length": str(size),
}).raise_for_status()
# 3. poll until done, then read the verdict
while True:
check = requests.get(f"{API}/checks/{check['id']}", headers=H,
params={"summary": "0"}).json()
if check["status"] in ("done", "error"):
break
time.sleep(5)
for item in check["items"]:
print(item["filename"], item["verdict"], item["headline"])
if item["evidence_url"]:
evidence = requests.get(item["evidence_url"], headers=H).json()
import { readFile } from "node:fs/promises";
const API = "https://ky.ax/api/v1";
const headers = { Authorization: `Bearer ${process.env.KY_API_KEY}`, "X-Ky-Client": "My App" };
const call = async (url, init = {}) => {
const r = await fetch(url, { ...init, headers: { ...headers, ...init.headers } });
const body = await r.json();
if (!r.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
return body;
};
// 1. declare the song
const bytes = await readFile("my-song.mp3");
let check = await call(`${API}/check`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mode: "catalogue", files: [{ name: "my-song.mp3", size: bytes.length }] }),
});
// 2. send it
await call("https://ky.ax" + check.items[0].upload_url, {
method: "PUT",
headers: { "Content-Type": "application/octet-stream" },
body: bytes,
});
// 3. poll until done, then read the verdict
for (;;) {
check = await call(`${API}/checks/${check.id}?summary=0`);
if (check.status === "done" || check.status === "error") break;
await new Promise((r) => setTimeout(r, 5000));
}
for (const item of check.items) {
console.log(item.filename, item.verdict, item.headline);
if (item.evidence_url) await call(item.evidence_url);
}
{
"id": "c_8Hq2x0VnY1aK3mPq7RtZ9w",
"mode": "catalogue",
"items": [
{ "id": "i_Zp4mWq0eT7yLk2Nn5bVc8g", "kind": "file", "filename": "my-song.mp3",
"size": 4812394, "media": "audio", "side": "a", "status": "uploading", "verdict": null,
"upload_url": "/api/v1/checks/c_8Hq2x0VnY1aK3mPq7RtZ9w/items/i_Zp4mWq0eT7yLk2Nn5bVc8g/upload" }
],
"url": "https://ky.ax/r/c_8Hq2x0VnY1aK3mPq7RtZ9w",
"poll": "https://ky.ax/api/v1/checks/c_8Hq2x0VnY1aK3mPq7RtZ9w"
}
Uploading
There are two ways in. Songs and stems, in any common format, go in two steps, so a large file is streamed rather than held; MIDI can go in one request.
Songs and stems in two steps
- Declare the files:
POST /api/v1/checkwith JSON{"mode", "files": [{"name", "size", "type"}]}. Each file item comes back with anupload_url. - Send each file:
PUTits raw bytes to theupload_url(on the host you called), with exactly the declared size asContent-Length.
The name's extension (or the optional type) says what a file is, and its first bytes must agree, or the upload is refused with 415. MIDI files may use the two steps too. A declared file that isn't sent within an hour is dropped. Ky transcribes a recording's sung melody, bass line, an instrumental line, chords, drums and words and compares them, so a Clear on a single stem covers only what Ky could hear in that stem.
MIDI in one request
Send multipart/form-data to POST /api/v1/check, one part per file (any field name), and an optional mode field. Or send a single file as the body with Content-Type: audio/midi and its name in X-Ky-Filename. A recording sent this way is refused with 415 recording_two_step.
Formats and limits
| Songs and stems | MIDI | |
|---|---|---|
| Formats | MP3, WAV, FLAC, M4A, AIFF, Ogg, Opus, WMA, WebM | .mid, .midi |
| Largest file | 100 MB (the platform's request ceiling) | 100 MB |
| Longest recording | 10 minutes (see below) | — |
| A check | up to 1,000 files (a technical ceiling, not an allowance) | up to 1,000 files |
| A request | one file per PUT | 50 MB and 100 files in one multipart request (use the two steps for more) |
| A day | no daily limit at present | no daily limit at present |
| Kept | deleted once Ky has finished with it (its check and any comparison), within 24 hours in any case | with your check until you delete it |
A recording longer than 10 minutes is accepted but not transcribed: checked against the catalogue, its item ends in error saying so; in a comparison, the other files are compared without it and the comparison's result says why. The other current values are always at GET /api/status (0 means no limit beyond the ceilings above).
# declare them together, then send each one
curl https://ky.ax/api/v1/check \
-H "Authorization: Bearer $KY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"mode": "catalogue", "files": [
{"name": "verse-take-2.wav", "size": 31457280},
{"name": "vocal-stem.flac", "size": 9830400}]}'
# each item in the reply carries its own upload_url
curl --upload-file verse-take-2.wav \
"https://ky.ax/api/v1/checks/c_…/items/i_…/upload" \
-H "Authorization: Bearer $KY_API_KEY" \
-H "Content-Type: application/octet-stream"
paths = ["verse-take-2.wav", "vocal-stem.flac"]
r = requests.post(f"{API}/check", headers=H, json={
"mode": "catalogue",
"files": [{"name": p, "size": os.path.getsize(p)} for p in paths],
})
r.raise_for_status()
for path, item in zip(paths, r.json()["items"]):
with open(path, "rb") as f:
requests.put("https://ky.ax" + item["upload_url"], data=f, headers={
**H, "Content-Type": "application/octet-stream",
"Content-Length": str(os.path.getsize(path)),
}).raise_for_status()
import { readFile, stat } from "node:fs/promises";
const paths = ["verse-take-2.wav", "vocal-stem.flac"];
const files = await Promise.all(paths.map(async (name) => ({ name, size: (await stat(name)).size })));
const check = await call(`${API}/check`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mode: "catalogue", files }),
});
for (const [i, item] of check.items.entries()) {
await call("https://ky.ax" + item.upload_url, {
method: "PUT",
headers: { "Content-Type": "application/octet-stream" },
body: await readFile(paths[i]),
});
}
curl https://ky.ax/api/v1/check \
-H "Authorization: Bearer $KY_API_KEY" \
-F "file=@verse.mid" \
-F "file=@chorus.mid"
# or one file as the body
curl https://ky.ax/api/v1/check \
-H "Authorization: Bearer $KY_API_KEY" \
-H "Content-Type: audio/midi" \
-H "X-Ky-Filename: my-song.mid" \
--data-binary @my-song.mid
files = [("file", (name, open(name, "rb"), "audio/midi"))
for name in ["verse.mid", "chorus.mid"]]
r = requests.post("https://ky.ax/api/v1/check", headers=H, files=files)
const form = new FormData();
for (const name of ["verse.mid", "chorus.mid"]) {
form.append("file", new Blob([await readFile(name)], { type: "audio/midi" }), name);
}
await fetch("https://ky.ax/api/v1/check", { method: "POST", headers, body: form });
Checks and results
GET /api/v1/checks/{id} returns the check and every item. A check and each item move through these statuses:
uploadingqueuedrunningdoneerror
Poll every 2 to 3 seconds until the check is done or error. A MIDI file is usually checked within a minute; a recording usually takes one to three, as its notes are written down first. engine.ahead_midi and engine.ahead_audio say how many files are waiting ahead of yours. A file identical to one you checked recently, with the same engine release, comes back done at once.
The four verdicts
Show the words, never the codes, and never words like “safe”, “pass”, “fail” or “infringing”.
- Clear
clearNothing of note found in what Ky has.
- Worth a look
lookSomething resembles another work: a short or common passage, or an element such as the chords or rhythm. Have a listen.
- Likely shared material
sharedA long, uncommon passage lines up closely, or the melody, the words or several elements are closely alike.
- Looks like a version
versionMost of the piece lines up with one work, or its melody and more are closely alike, as a cover or arrangement would be. If it's yours, that's expected.
Results describe musical resemblance: melody and bass line, chords, rhythm, form and words. They are not a legal conclusion and not legal advice. Link the Results Disclaimer wherever you show one.
An item
verdict | One of the four codes, or null until done. |
|---|---|
headline | One plain sentence about the result. |
explanation | A short account of the numbers in plain English. |
findings | Up to 10 readings that count, the headline's first (primary): the work, how the passages relate, the voice, how much of yours lines up and where, and how common the material is. A held or repeated note never counts. |
workflow | A brief of the detailed comparison: which techniques ran and what they found. |
url | The item on its result page, to offer people. |
evidence_url | Once done, the full evidence record: every measure for every ranked match. |
summary | Everything the result page shows, as the engine wrote it, piano-roll exhibits included. Leave it out with ?summary=0. |
error | Why an item couldn't be checked, fit to show. |
The evidence record and your MIDI file are also at …/items/{item}/result and …/items/{item}/file. A recording is deleted once Ky has finished with it, so there is no file to download for one. The whole check as a PDF — the results, the passages that lined up, the numbers and the piano rolls, with a disclaimer line on every page and the Results Disclaimer in full at the end — is at …/checks/{id}/report.pdf.
curl https://ky.ax/api/v1/checks/c_8Hq2x0VnY1aK3mPq7RtZ9w \
-H "Authorization: Bearer $KY_API_KEY"
check = requests.get(f"{API}/checks/{check_id}", headers=H).json()
for item in check["items"]:
if item["status"] == "done":
print(item["verdict"], item["headline"])
for f in item["findings"]:
print(" ", f["work"]["title"], f["relationship"], f["proportion_of_yours"])
const check = await call(`${API}/checks/${id}`);
for (const item of check.items.filter((i) => i.status === "done")) {
console.log(item.verdict, item.headline);
for (const f of item.findings) console.log(" ", f.work.title, f.relationship, f.proportion_of_yours);
}
{
"id": "c_8Hq2x0VnY1aK3mPq7RtZ9w",
"status": "done",
"mode": "catalogue",
"source": "plugin:My App",
"created_at": 1793098914,
"updated_at": 1793098921,
"url": "https://ky.ax/r/c_8Hq2x0VnY1aK3mPq7RtZ9w",
"engine": { "online": true, "ahead": 0, "ahead_midi": 0, "ahead_audio": 0, "audio_online": true },
"items": [
{
"id": "i_Zp4mWq0eT7yLk2Nn5bVc8g",
"kind": "file",
"media": "midi",
"filename": "my-song.mid",
"status": "done",
"verdict": "look",
"headline": "Worth a look: bars 3–4 of your melody resemble “Example Tune” (Example Writer, 1971); that material is uncommon in the catalogue.",
"explanation": "Eight notes in bars 3–4 of your melody line up with bars 9–10 of “Example Tune”. Material like this is uncommon in the catalogue.",
"findings": [
{
"primary": true,
"work": { "title": "Example Tune", "artist": "Example Writer", "year": 1971 },
"relationship": "shared passage",
"voice": "melody",
"aligned_notes": 8,
"proportion_of_yours": 0.12,
"bars_yours": [3, 4],
"bars_theirs": [9, 10],
"commonness": "uncommon",
"has_exhibit": true
}
],
"url": "https://ky.ax/r/c_8Hq2x0VnY1aK3mPq7RtZ9w#i_Zp4mWq0eT7yLk2Nn5bVc8g",
"evidence_url": "https://ky.ax/api/v1/checks/c_8Hq2x0VnY1aK3mPq7RtZ9w/items/i_Zp4mWq0eT7yLk2Nn5bVc8g/result",
"summary": { "…": "…" }
}
]
}
Modes
A check's mode says what each file is compared with. Give it as the mode field of a declaration or a multipart request, as ?mode=, or in X-Ky-Mode.
catalogue | Each file against the catalogue. The default. |
|---|---|
pairwise | Your files against each other (two or more songs, stems or MIDI files; a recording is transcribed first and compared on its transcription and, between recordings, on its sound). One extra item of kind: "pairwise" carries the result; its findings are pairs of your files. |
both | The two together: each file against the catalogue, and the files against each other. |
Comparing your songs with a second set is on ky.ax only for now; through the API it answers 400 sets_web_only.
curl https://ky.ax/api/v1/check \
-H "Authorization: Bearer $KY_API_KEY" \
-F "mode=pairwise" \
-F "file=@take-1.mid" \
-F "file=@take-2.mid" \
-F "file=@take-3.mid"
names = ["take-1.mid", "take-2.mid", "take-3.mid"]
r = requests.post(f"{API}/check", headers=H, data={"mode": "pairwise"},
files=[("file", (n, open(n, "rb"), "audio/midi")) for n in names])
const form = new FormData();
form.append("mode", "pairwise");
for (const n of ["take-1.mid", "take-2.mid", "take-3.mid"]) {
form.append("file", new Blob([await readFile(n)], { type: "audio/midi" }), n);
}
await call(`${API}/check`, { method: "POST", body: form });
Endpoints
Every endpoint takes your key, except where it says public; those marked full access need a key made with it. The OpenAPI document has each one's parameters, schemas and examples.
- POST
/api/v1/device/startStart a device-code connection public - POST
/api/v1/device/pollPoll it for a key public - GET
/api/v1/meWho the key belongs to - POST
/api/v1/checkCreate a check (multipart, a MIDI body, or a declaration) - PUT
/api/v1/checks/{id}/items/{item}/uploadSend a declared file - GET
/api/v1/checksList your checks full access - GET
/api/v1/checks/{id}A check and its results - DELETE
/api/v1/checks/{id}Delete a check full access - GET
/api/v1/checks/{id}/items/{item}/resultAn item's evidence record - GET
/api/v1/checks/{id}/report.pdfThe similarity report (PDF) - GET
/api/v1/checks/{id}/items/{item}/fileA checked MIDI file full access - GET
/api/v1/usageUsage this period full access - GET
/api/v1/billingYour plan full access - GET
/api/statusEngine, queue and current limits public - GET
/api/v1/openapi.jsonThe OpenAPI 3.1 document public
Listing and deleting
GET /api/v1/checks lists your checks, newest first, with how many files got each verdict. It takes limit (1 to 100, default 50). When there are more, pass next_cursor back as cursor; it is null on the last page.
DELETE /api/v1/checks/{id} removes a check, its stored files and its evidence records for good. Files it used still count towards the period's usage.
curl "https://ky.ax/api/v1/checks?limit=20" \
-H "Authorization: Bearer $KY_API_KEY"
curl -X DELETE https://ky.ax/api/v1/checks/c_8Hq2x0VnY1aK3mPq7RtZ9w \
-H "Authorization: Bearer $KY_API_KEY"
checks, cursor = [], None
while True:
page = requests.get(f"{API}/checks", headers=H,
params={"limit": 100, **({"cursor": cursor} if cursor else {})}).json()
checks += page["checks"]
cursor = page["next_cursor"]
if not cursor:
break
requests.delete(f"{API}/checks/{checks[-1]['id']}", headers=H).raise_for_status()
const checks = [];
let cursor = null;
do {
const q = new URLSearchParams({ limit: "100", ...(cursor ? { cursor } : {}) });
const page = await call(`${API}/checks?${q}`);
checks.push(...page.checks);
cursor = page.next_cursor;
} while (cursor);
await call(`${API}/checks/${checks.at(-1).id}`, { method: "DELETE" });
{
"checks": [
{
"id": "c_8Hq2x0VnY1aK3mPq7RtZ9w",
"status": "done",
"mode": "catalogue",
"source": "plugin:My App",
"created_at": 1793098914,
"n_items": 1,
"first_file": "my-song.mid",
"verdicts": { "clear": 0, "look": 1, "shared": 0, "version": 0 },
"pair_verdict": null,
"url": "https://ky.ax/r/c_8Hq2x0VnY1aK3mPq7RtZ9w"
}
],
"next_cursor": null
}
Usage and billing
GET /api/v1/usage gives what your account has used this period: files against the plan's allowance (every file counts: a song, a stem or a MIDI file), how many were recordings and how many MIDI, checks by kind, and files per day. It is the same data as the Usage page in your account.
GET /api/v1/billing gives your plan and its state: which plan, monthly or yearly, when it renews or ends, and the extra files left. Your card and invoices stay on the Billing page, never in the API. Both need a key with full access.
Ky is free while it's in beta, and paid plans aren't open yet. So today both read "billing": "off" and "plan": "free", and nothing is enforced ("enforced": false); files are still counted, so you can see them. The planned plans are on the Pricing page.
curl https://ky.ax/api/v1/usage \
-H "Authorization: Bearer $KY_API_KEY"
curl https://ky.ax/api/v1/billing \
-H "Authorization: Bearer $KY_API_KEY"
usage = requests.get(f"{API}/usage", headers=H).json()
f = usage["files"]
print(f"{f['used']} files used, {f['left']} left this period")
const usage = await call(`${API}/usage`);
const { used, left } = usage.files;
console.log(`${used} files used, ${left} left this period`);
{
"billing": "off",
"enforced": false,
"plan": "free",
"period": { "start": 1790812800, "end": 1793491200, "kind": "calendar_month" },
"files": { "used": 8, "allowance": 20, "left": 12, "extra_left": 0, "recordings": 3, "midi": 5 },
"checks": { "total": 12, "catalogue": 11, "each_other": 1, "another_set": 0 },
"daily": [{ "date": "2026-10-01", "files": 0 }, "…"],
"resets_at": 1793491200
}
Errors
Anything but a 2xx carries the same JSON: a stable code for your program, and a message in plain English that you can show people as it is.
| Status | Codes | What to do |
|---|---|---|
| 400 | no_files too_many need_two too_many_recordings sets_web_only empty size_mismatch incomplete bad_request bad_cursor | Fix the request; the message says how. |
| 401 | unauthenticated | Send a valid key. A revoked key is refused. |
| 403 | terms scope | Accept the Terms on ky.ax once, signed in; or the key lacks full access for this call. |
| 404 | not_found | No such check or item, or it isn't yours. |
| 409 | uploaded busy | That file is already sent, or being sent. |
| 410 | gone | The upload expired: declare the file again. |
| 411 | length_required | Send a Content-Length. |
| 413 | too_large too_large_total | Send smaller files or fewer at a time. |
| 415 | not_midi not_audio recording_two_step unsupported audio_off | Check the format; send recordings in two steps. |
| 429 | quota audio_quota throttled | An allowance is used up. Daily ones reset at midnight UTC. |
| 503 | audio_busy | Too many recordings are waiting (not applied at present). Try again in a few minutes; MIDI still works. |
When paid plans open, 403 set_too_large and 429 files_used can also come back.
{
"error": {
"code": "recording_two_step",
"message": "demo.mp3 is a recording. Recordings are sent in two steps: declare them, then send each file (see https://ky.ax/plugins)."
}
}
Rate limits
Ky's edge allows 3,000 requests a minute to /api/* from one address, then refuses for a minute with 429; that answer comes from the edge, so it may not carry the JSON error shape. Poll a check every few seconds and you will never meet it. What else applies:
- No daily allowances at present (when there are, a refusal is
429 audio_quotaor429 quota, reset at midnight UTC). - No site-wide ceiling on waiting recordings at present (when there is one, new recordings may be refused for a while with
503 audio_busy). A recording not checked within a day is deleted and its item ends inerror. - 30 device-code connections an hour from one network (
429 throttled). - The per-file and per-check limits under Uploading.
Be kind to the engines: poll a check every 2 to 3 seconds, not faster, and stop when it's done. GET /api/status is public and gives the current limits; a limit of 0 means none applies.
curl https://ky.ax/api/status
status = requests.get("https://ky.ax/api/status").json()
print(status["audio"]["max_bytes"], status["audio"]["daily_user"])
const status = await (await fetch("https://ky.ax/api/status")).json();
console.log(status.audio.max_bytes, status.audio.daily_user);
{
"beta": true,
"engine": { "online": true, "queue": 2, "queue_midi": 2, "queue_audio": 0, "…": "…" },
"limits": { "files_per_check": 0, "file_bytes": 104857600, "daily_anon": 0, "daily_user": 0 },
"audio": { "enabled": true, "max_bytes": 104857600, "per_check": 1000,
"daily_anon": 0, "daily_user": 0, "engine_online": true },
"sets": { "enabled": true, "per_set": 1000 }
}
Webhooks
Not yet. For now, poll GET /api/v1/checks/{id} until it's done. If you need Ky to call you when a check finishes, tell us at hello@ky.ax.
Changelog
Two new elements can appear in a result's
families:sample(recorded sound: the same recorded sound in two recordings) andversion(music as recorded, compared with a model trained on versions of songs), with their techniques "Recorded sound comparison" and "Comparison of the music as recorded".A recording can be up to 10 minutes long (it was an hour). A longer one is accepted but not transcribed: see Formats and limits. A comparison with no pair left to compare ends in
errorsaying why, instead of a verdict.This page and the OpenAPI document. Keys now have a scope: plugins and existing keys send files and read their own checks; a key with full access, made under Plugins & API keys, can list, download, delete, and read usage and billing. New: list checks with a cursor, delete a check, download an item's evidence record and MIDI file, usage and billing. Each item now carries its
explanation,findingsin short, itsurlandevidence_url;?summary=0leaves the full summary out.Recordings, through the two-step upload.
Version 1: device-code connect,
POST /api/v1/checkfor MIDI, and results.
Version 1 changes only by adding: new fields and endpoints may appear, and existing ones keep their meaning.