Konnect API
Upload, price and publish your beats from your own scripts. Your catalogue is yours — move it in and out whenever you like.
Get an API keyYou don't need to write code to automate uploads. Most producers use the watch folder — point it at the folder you export to and new beats upload themselves, with subfolder names setting the type-beat tags. This page is for the rest: if you already automate things, everything below is yours.
Authentication
Send your key on every request. Either header works:
X-API-Key: kn_live_...
Authorization: Bearer kn_live_...
Keys are shown once when created and stored only as a hash, so we cannot recover one for you — mint a new key and revoke the old one. Create and revoke keys on your Store page.
Scopes
| Scope | Grants |
|---|---|
| beats:read | Read your listings. |
| beats:write | Create, update, publish and delete listings. |
| scrape:read | Reserved. Not yet accepted by any endpoint. |
| scrape:write | Reserved. Not yet accepted by any endpoint. |
Uploading a beat
Four calls: create the listing, ask where the audio goes, send the bytes,
then publish. A listing stays draft until you publish it, so
nothing is ever visible before its audio has landed.
# 1. Create the draft. Nothing is public yet.
BEAT=$(curl -s -X POST https://kiddynaconnect.com/api/v1/beats \
-H "X-API-Key: $KONNECT_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"title":"Midnight Drive","bpm":140,"key":"F#m",
"tags":["dark","trap"],
"licenses":[{"tier":"MP3 Lease","price_cents":2999}]}')
ID=$(echo "$BEAT" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
# 2. Ask where the audio goes. You get back a URL to PUT to -- a presigned
# bucket URL if storage is configured, otherwise this app. Same client code.
URL=$(curl -s -X POST https://kiddynaconnect.com/api/v1/beats/$ID/audio \
-H "X-API-Key: $KONNECT_KEY" \
-H "Content-Type: application/json" \
-d '{"filename":"midnight-drive.wav"}' \
| grep -o '"url":"[^"]*"' | cut -d'"' -f4)
# 3. Send the bytes. Raw body, no multipart.
curl -s -X PUT "$URL" \
-H "X-API-Key: $KONNECT_KEY" \
--upload-file ./midnight-drive.wav
# 4. Go live. This is the only step that makes it visible on your store.
curl -s -X PATCH https://kiddynaconnect.com/api/v1/beats/$ID \
-H "X-API-Key: $KONNECT_KEY" \
-H "Content-Type: application/json" \
-d '{"visibility":"public"}'
Endpoints
| Path | Scope | ||
|---|---|---|---|
| POST | /api/v1/beats | beats:write | Create a draft listing (metadata only). |
| POST | /api/v1/beats/{id}/audio | beats:write | Ask where to send the audio. Returns a URL to PUT the file to. |
| PUT | /api/v1/beats/{id}/audio | beats:write | The fallback upload target when object storage is not configured. |
| PATCH | /api/v1/beats/{id} | beats:write | Update metadata, set prices, or publish. |
| GET | /api/v1/beats | beats:read | List your beats, newest first. Cursor paginated. |
| GET | /api/v1/beats/{id} | beats:read | Fetch one beat. |
| DELETE | /api/v1/beats/{id} | beats:write | Delete a beat and its audio. |
Prices are integer cents
A licence price is a whole number of cents — 2999, not
29.99. Sending a fractional value is rejected rather than
rounded, because silently turning 29.99 into
29.989999999999998 is how storefronts end up a cent short.
Visibility
One of draft, unlisted, public. Only public appears on your store page;
unlisted is reachable by direct link.
Idempotency
Send an Idempotency-Key header on POST and a
retry returns the original response instead of creating a second listing.
Keys are remembered for 24 hours. Use one per logical upload — a fresh
UUID per beat, reused across that beat's retries.
Pagination
Lists are cursor-based. Pass limit (max 100) and
after, and read the next cursor from the response envelope.
Cursors are stable as your catalogue changes; offsets are not, which is
why there is no page parameter.
GET /api/v1/beats?limit=50&after=1042
Rate limits
120 requests per 60 seconds, per key — so one script hitting its ceiling does not stop your others. Every response carries your remaining budget, not just the rejections:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 1786125810
Over the limit you get 429 with the usual error envelope and
a Retry-After header in seconds. Rejected requests are not
counted against you, so backing off actually clears the window.
Errors
Every error uses one envelope. code is stable and safe to
branch on; message is for humans and may change.
{
"error": {
"code": "invalid_request",
"message": "title is required.",
"field": "title"
}
}
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Something in the body is wrong. `field` names which. |
| 401 | unauthorized | Missing or unrecognised key. |
| 403 | insufficient_scope | Valid key, but it lacks the scope this route needs. |
| 404 | not_found | No such beat, or not yours. |
| 409 | conflict | The request collides with the current state. |
| 422 | beat_not_ready | The listing has no audio yet, so it cannot go public. |
| 429 | rate_limited | Over the per-key limit. Retry after the window resets. |
| 500 | server_error | Our fault. Safe to retry. |
Still early. The beats endpoints are stable and
versioned under /api/v1. The scrape:* scopes
are reserved and not yet accepted anywhere. There are no webhooks
yet — poll GET /api/v1/beats if you need upload status.