Overview
The Track Anatomy API creates analyses from an audio file or a public YouTube link and returns the same report shown on the website. All responses are JSON. The base URL is https://trackanatomy.com/api/v1.
Create and manage keys on the API keys page. Send a key in the Authorization header:
Authorization: Bearer ta_live_...
Keys need an active credit plan. A key is shown once when it is created, can be revoked at any time and carries its own request, concurrency and daily-spend limits. Never put a key in a query string.
Credits work as they do on the website. An upload costs 10 credits and a link analysis costs 15. Each analysis includes one lyrics set; further sets for the same analysis cost 3 credits. The account daily limit of 120 credits applies across the website and the API.
An analysis response includes lyrics.includedLeft and lyrics.extraPrice. A lyrics response includes credits.included; together they show whether a set is included and what another set will cost.
Quick start
- Create an API key on the API keys page and keep the full key in a server-side secret store.
- Create an analysis with a file or a public link.
- Poll the analysis by id until
statusissucceededorfailed. - Read
reportfrom the completed analysis, or use a signed webhook instead of polling.
Create from a file
curl -X POST https://trackanatomy.com/api/v1/analyses \
-H "Authorization: Bearer $TRACK_ANATOMY_KEY" \
-H "Idempotency-Key: upload-2026-10-30-001" \
-F "file=@track.mp3" \
-F "webhook_url=https://example.com/hooks/track-anatomy"Create from a link
curl -X POST https://trackanatomy.com/api/v1/analyses \
-H "Authorization: Bearer $TRACK_ANATOMY_KEY" \
-H "Content-Type: application/json" \
-d '{"link":"https://www.youtube.com/watch?v=abcdefghijk"}'Poll
curl https://trackanatomy.com/api/v1/analyses/an_example \
-H "Authorization: Bearer $TRACK_ANATOMY_KEY"A new job answers 202. Poll no more often than every few seconds. A cached link report can answer 200 with the report already complete. Poll until status is succeeded or failed; then read report or error.
Endpoints
| Method | Path | Purpose | Common statuses |
|---|---|---|---|
POST | /analyses | Create an upload or link analysis. | 202, 200, 400, 402, 413, 422, 429 |
GET | /analyses/{id} | Read status and the report when complete. | 200, 401, 404 |
GET | /analyses | List analyses, newest first. | 200, 400 |
DELETE | /analyses/{id} | Remove a finished analysis from history. | 204, 400, 404 |
POST | /analyses/{id}/lyrics | Write a lyrics set for a completed analysis. | 202, 400, 402, 404, 429 |
GET | /lyrics/{id} | Read a lyrics job and its result. | 200, 404 |
GET | /analyses/{id}/lyrics | List completed lyrics sets for an analysis. | 200, 404 |
GET | /account | Read plan, credits and account usage. | 200, 401 |
Create an analysis
POST /analyses
Send either multipart/form-data with a file field, or JSON with a link field. Public YouTube videos are accepted. The first 10 minutes of a link are used.
API keys can add webhook_url. It must be an HTTPS URL on port 443, without credentials. Private, loopback, link-local, multicast and carrier-grade NAT addresses cannot be targets.
Add Idempotency-Key to make retries safe. A fresh job answers 202. A cached link report can answer 200 with the report already included.
Read an analysis
GET /analyses/{id}
The response is an Analysis object. While a job is queued or running, report is null. When it completes, report is a report object. On failure, error carries a stable code and a plain message.
List analyses
GET /analyses?limit=20&cursor=...&type=upload&q=...
limit is 1 to 100. type is upload or link. q searches the caller's analyses. Follow nextCursor until it is null.
Delete an analysis
DELETE /analyses/{id}
Deletes a finished analysis and its lyrics sets from history. It returns 204. A queued or running analysis cannot be deleted.
Create lyrics
POST /analyses/{id}/lyrics
The JSON body can contain genre, theme, language, style and, for API keys, webhook_url. Each string is optional and trimmed before use. The job answers 202; poll GET /lyrics/{id} or wait for a webhook.
Read lyrics
GET /lyrics/{id} returns a Lyrics object. GET /analyses/{id}/lyrics returns { "data": [...] } with every completed set for that analysis, newest first.
Read the account
GET /account returns the key owner's email, active plan, next plan, available credits, usage today and free-test count. API-key usage consumes the same account credits and obeys the account daily limit.
Objects
Analysis
| Field | Meaning |
|---|---|
id | Analysis id used with every later request. |
object | Always analysis. |
type | upload or link. |
status | queued, running, succeeded or failed. |
stage | The current progress stage, or null after finishing. |
createdAt, startedAt, finishedAt | UTC timestamps in ISO 8601 form. |
input | File name, size and duration for uploads; link, video id and title for links. |
credits | Price, whether a free test was used, and whether the hold was charged. |
error | null, or { code, message } when the analysis failed. |
report | null until success, then a full ta.report.v1 object. |
lyrics | includedLeft and extraPrice for lyrics sets. |
An AnalysisSummary has the same base fields plus title, tempo and key. List responses use this smaller shape.
Lyrics
| Field | Meaning |
|---|---|
id | Lyrics job id. |
object | Always lyrics. |
analysisId | The analysis this set was written for. |
status, stage | Current job state and progress stage. |
createdAt, finishedAt | UTC timestamps in ISO 8601 form. |
options | Requested genre, theme and language. |
style | The style text used to shape the set. |
credits | price, charged and included. |
error | null, or { code, message } after a failure. |
lyrics | null until success, then { text, tags }. |
Account
| Field | Meaning |
|---|---|
email, emailVerified | The account address and whether it is verified. |
plan, nextPlan | The active plan period and the next queued period, or null. |
credits.balance | All remaining credits, including credits held by running jobs. |
credits.available | Credits available to start another job. |
credits.usedToday | Credits charged today, in UTC. |
credits.dailyCap | The account's daily credit limit. |
freeTests.uploads | Website free uploads left. API keys never use free tests. |
prices.upload, prices.link | Credits for an upload and for a link analysis. |
studio | The Track Anatomy Studio plan, the next and the last one ended, the free trial and the browsers signed in; used by the studio. |
studio.plan, studio.nextPlan | The active Studio plan and the next stacked plan, or null. |
studio.ended | The last ended Studio plan while none is active, or null. |
studio.trial | Whether the free trial is available and its length in days. |
studio.devices | Studio browsers used and the account max. |
Report
Every report has schema: "ta.report.v1".
| Field | Meaning |
|---|---|
id, analysisId | Report and analysis ids. |
input | Input type, title, duration, analyzed window and optional link details. |
createdAt | When the report was created, in UTC. |
core.tempo | bpm, feel, certainty, optional confidence and note. |
core.meter | Meter value, certainty, optional confidence and note. |
core.groove | Groove value, certainty, optional confidence and note. |
core.key | Key name, certainty, optional confidence, note and alternative key. |
core.chords | Chord progression, Roman numerals, certainty, note and timeline. |
core.loudness | Integrated loudness, range, dynamic range, certainty and description. |
sections | Section name, start time and short notes. |
genre | Primary genre, secondary genres and a one-sentence summary. |
descriptors | Mood, rhythm, harmony, genre, instrument, drums and bass, vocal, arrangement, mix and production descriptor lists. |
vocals | Lead style and language. |
notes | Identity, harmony, rhythm, topline, sound and dynamics paragraphs. |
takeaways | Practical ideas for a new track in this style. |
notCertain | Details the report could not determine clearly. |
stylePrompts.full | Full style prompt text and its character count. |
stylePrompts.core | Short measured style prompt for uploads; absent for links. |
certainty is one of:
| Value | Meaning |
|---|---|
measured | Measured from the file. |
estimated | Estimated from the link. |
not_certain | No clear answer. |
Webhooks
An API key can attach webhook_url when it creates an analysis or lyrics set. Track Anatomy sends a signed POST request after the job succeeds or fails.
| Event | Meaning |
|---|---|
analysis.finished | An upload or link analysis succeeded or failed. |
lyrics.finished | A lyrics job succeeded or failed. |
The body is the same object returned by the matching GET endpoint, plus an event field. Read the job status before reading report or lyrics; a failed job includes error.
Every request has these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | TrackAnatomy-Webhooks/1 |
TA-Signature | t=<unix seconds>,v1=<HMAC-SHA256 hex> |
The signed content is <timestamp>.<raw body> using the webhook secret shown when the key was created. Compare the signature in constant time and reject timestamps outside five minutes.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody, signatureHeader, secret) {
const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(signatureHeader ?? '');
if (!match) return false;
const timestamp = Number(match[1]);
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 300) return false;
const expected = createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest();
const received = Buffer.from(match[2], 'hex');
return received.length === expected.length && timingSafeEqual(received, expected);
}Answer with any 2xx status within 10 seconds. Redirects are not followed and count as failures. Failed requests retry after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. After the sixth failed attempt the delivery stops.
The URL must use HTTPS on port 443 and must not contain credentials. A hostname that resolves to a private, loopback, link-local, carrier-grade NAT or multicast address is refused: nothing is sent and the delivery is not retried.
Errors
Every error has the same JSON shape:
{
"error": {
"code": "validation_error",
"message": "Some details are missing or not valid."
}
}| Code | Status | Meaning |
|---|---|---|
unauthorized | 401 | The key is missing, invalid or revoked. |
forbidden | 403 | The caller cannot use this resource. |
not_found | 404 | The resource does not exist for this account. |
validation_error | 400 | A field is missing or invalid. |
unsupported_format | 415 | The file type is not supported. |
file_too_large | 413 | The upload exceeds the configured size limit. |
too_long | 422 | The track is longer than the configured limit. |
too_short | 422 | The track is too short to analyze. |
invalid_link | 422 | The value is not a supported public YouTube link. |
link_not_public | 422 | The video is private or unlisted. |
link_unavailable | 422 | The video is removed or restricted. |
email_not_verified | 403 | The account must verify its email first. |
plan_required | 402 | The account needs an active credit plan. |
insufficient_credits | 402 | The account does not have enough credits. |
daily_limit_reached | 429 | The account or key has reached its daily limit. |
too_many_running | 429 | The key or account has reached its concurrency limit. |
rate_limited | 429 | Requests are arriving too quickly. |
idempotency_conflict | 409 | The key was already used for a different body. |
busy | 503 | The service is busy. Nothing was charged. |
analysis_failed | 500 | The analysis failed. Nothing was charged. |
A 429 response includes Retry-After in seconds. Do not retry other 4xx responses without changing the request.
Limits and idempotency
Each API key has three limits:
- Requests per minute. A
429response includesRetry-After. - Concurrent queued and running jobs.
- An optional daily credit spend limit set by the customer.
The account daily limit of 120 credits also applies. Uploads and links use the same credit balance as the website, and all limits reset on UTC boundaries.
Send Idempotency-Key on every create request that may be retried. A key is stored for 24 hours.
| Retry | Result |
|---|---|
| Same key and same request | The original job and response, with Idempotent-Replayed: true |
| Same key and different request | 409 idempotency_conflict |
| No key | A new job every time |
For multipart uploads, the file content and field values are compared. Keep request field values identical when retrying. Do not reuse a key for a different analysis or lyrics request.
Changelog
v1 - initial release
- File and public-link analyses
- Reports, lyrics and account usage
- Signed webhooks with retries
- Per-key request, concurrency and daily-spend limits
- Idempotent create requests