Access
The API is part of the Studio plan ($19/mo). After upgrading, head to /account/api-keys to mint a key. Each key carries its own webhook signing secret.
- Up to 5 active keys per account
- 30 requests / minute (sliding window)
- 1,000 calls / month (calendar month, UTC)
- HMAC-SHA256 signed webhooks for async result delivery
Quick start
# Submit a file for analysis
curl -X POST https://signalkey.io/api/v1/analyze \
-H "Authorization: Bearer sk_live_..." \
-F "file=@track.mp3" \
-F "webhook_url=https://your-app.com/hook"
# Response:
{
"job_id": "abc123",
"object": "analysis_job",
"status": "queued",
"stream_url": "/api/v1/jobs/abc123/stream",
"poll_url": "/api/v1/jobs/abc123",
"webhook_delivery": "scheduled"
}Authentication
All /api/v1/* endpoints require a bearer token in the Authorization header:
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxx
Tokens are 32 characters of base64url after the sk_live_ prefix. We store only the SHA-256 hash — losing a token means revoking it and minting a fresh one. Tokens stop working the moment the owner falls below the Studio tier.
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST | /api/v1/analyze | Submit a file (multipart) for full analysis |
GET | /api/v1/jobs/{id}/stream | SSE progress + result stream |
GET | /api/v1/jobs/{id} | Poll the job state (returns 202 + retry-after while running, 200 with the result when done) |
GET | /api/v1/me | Authenticated key metadata + live rate-limit window |
Stream progress (SSE)
GET /api/v1/jobs/{job_id}/stream
Authorization: Bearer sk_live_...
event: progress
data: {"progress": 0.05, "stage": "upload"}
event: progress
data: {"progress": 0.40, "stage": "features"}
event: result
data: { "id": "...", "tempo": { "bpm": 124, ... }, ... }
event: done
data: {"ok": true}Webhooks
Pass webhook_url on the analyze request and we POST the result there once the pipeline finishes. Each delivery carries an X-SignalKey-Signature header you should verify before trusting the body.
X-SignalKey-Signature: t=1714325000,v1=<hex>
X-SignalKey-Event: analysis.completed
# Verify (Node):
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Each API key has its own webhook signing secret (revealed once at creation alongside the bearer token). Replay protection: signatures older than 300 s are rejected.
Rate limits
- Studio plan: 30 requests / minute (sliding window) · 1,000 calls / month (UTC calendar month)
- Studio Plus (custom): 120 / minute · 10,000 / month — contact us
- Enterprise: negotiated per contract
Every authenticated response carries the live state in headers:
X-RateLimit-Limit-Minute: 30 X-RateLimit-Remaining-Minute: 27 X-RateLimit-Reset-Minute: 1714325060 X-RateLimit-Limit-Month: 1000 X-RateLimit-Remaining-Month: 942 X-RateLimit-Reset-Month: 1717200000
When you exceed a window the response is 429 with a retry-after header (seconds until the next slot opens) and a JSON body identifying which scope was hit.
Result schema (excerpt)
{
"id": "...",
"file_meta": { "filename": "...", "duration_sec": 210, ... },
"tempo": { "bpm": 124, "confidence": 0.94, "time_signature": "4/4" },
"key": { "name": "C minor", "camelot": "5A", "open_key": "1m", "confidence": 0.88 },
"genre": { "primary": { "label": "House", "confidence": 0.82 }, "top5": [...] },
"mood": { "valence": 56, "energy": 74, "danceability": 78, "tags": [...] },
"loudness": { "integrated_lufs": -10.5, "true_peak_db": -0.6 },
"structure": { "sections": [...] },
"mix_report": { "lufs_integrated": -10.5, "checks": [...], "priority_move": "..." },
"create_report": { "chords": [...], "suggested_vocal_key": "C minor" },
"stems_report": { "stems": [{ "id": "vocals", "quality_pct": 92, ... }, ...] },
"release_kit": { "clips": [...], "captions": {...}, "hashtags": [...] }
}Errors
| Status | Meaning |
|---|---|
400 | Invalid request (missing file, bad parameters) |
401 | Missing, invalid or revoked API key |
403 | Owner has dropped below the Studio tier |
404 | Job not found (or not yours) |
413 | File too large for your plan |
415 | Unsupported content-type or audio format |
429 | Rate limit exceeded — see retry-after header |
500 | Analysis failed — retry the upload |
503 | Backend not configured (returned by mock environments) |