Tracks API Documentation
Tracks are the catalog itself: the title, lyrics, flags, tempo and duration of each recording, its composers and publishers with their shares, the artists it sounds like, and a value for each of the library’s metadata types.
Tracks are addressed by your own external ID. Metadata tags, composers and publishers can be created on the fly from the object forms described below, so a single track upload can carry everything the track needs, and the bulk endpoint takes up to 200 tracks at a time when loading a catalog. What GET gives, PUT and POST take, so a track read from the API can be sent back as it is; library_id, created_at, updated_at and url are accepted and ignored. Reading is open to every collaborator; creating, updating and deleting need the editor role.
Endpoints
GET /api/v1/libraries/{library_id}/tracks
Retrieve the tracks in a library, ordered by title. With q, only the tracks that have every word of it somewhere in their text: this finds tracks by what is written in them, where the search endpoint finds music by what it is like.
Path Parameters
| Name | Type | Description | Required |
|---|---|---|---|
| library_id | integer | ID of the library | Yes |
Query Parameters
| Name | Type | Description |
|---|---|---|
| page | integer | Page number for pagination (default: 1) |
| per_page | integer | Records per page, up to 200 (default: 25) |
| q | string | List only the tracks that have every word of this in their title, description, lyrics, tags or credits |
Response
{
"pagination": {
"total_entries": 34,
"per_page": 25,
"current_page": 1,
"total_pages": 2,
"next_page": 2,
"next_page_url": "https://app.spondulix.ai/api/v1/libraries/2/tracks?page=2"
},
"tracks": [
{
"id": 999,
"library_id": 2,
"title": "Drunken Daisy - You Gotta Have Banjo in the Band",
"description": "An energetic tune that is part bluegrass and part Irish folk dance...",
"lyrics": null,
"lyrics_chorus_only": null,
"active": true,
"instrumental": true,
"explicit": false,
"clean_available": false,
"public_domain": false,
"bpm": null,
"duration": 185,
"isrc": null,
"style_alikes": ["chieftains", "clancy brothers", "flatt and scruggs"],
"catalog_id": 2,
"genre_id": 3,
"subgenre_ids": [17, 169],
"mood_names": ["calm", "warm"],
"instrument_ids": [1960, 1943, 1782, 1698],
"release_date": "2019-04-12",
"created_at": "2024-08-19T12:42:55.454-05:00",
"updated_at": "2024-10-16T17:10:42.698-05:00",
"tracks_composers": [
{ "composer_id": 186, "share": "100.0" }
],
"tracks_publishers": [
{ "publisher_id": 2, "share": "50.0" },
{ "publisher_id": 37, "share": "50.0" }
],
"url": "https://app.spondulix.ai/api/v1/libraries/2/tracks/999"
}
]
}
Response Fields
| Field | Type | Description |
|---|---|---|
| id | integer | External ID of the track |
| library_id | integer | ID of the library the track belongs to |
| title | string | Title of the track (max 255 chars) |
| description | string | null | Description of the track (max 1500 chars) |
| lyrics | string | null | Full lyrics of the track (max 5000 chars) |
| lyrics_chorus_only | string | null | Chorus lyrics of the track (max 5000 chars) |
| active | boolean | Whether the track is active |
| instrumental | boolean | Whether the track is instrumental |
| explicit | boolean | Whether the track contains explicit content |
| clean_available | boolean | Whether a clean version is available |
| public_domain | boolean | Whether the track is in the public domain |
| bpm | integer | null | Beats per minute; null when unknown |
| duration | integer | null | Duration in seconds |
| isrc | string | null | International Standard Recording Code |
| style_alikes | array | Names of artists the track sounds like |
| {type}_id | integer | null | External ID of the tag, for each single-valued tag type that uses external IDs |
| {type}_ids | array | External IDs of the tags, for each multi-valued tag type that uses external IDs |
| {type}_name | string | null | Name of the tag, for each single-valued tag type that does not |
| {type}_names | array | Names of the tags, for each multi-valued tag type that does not |
| {type} | mixed | The value, for each non-tag metadata type. Multi-valued types use the plural name and hold an array. |
| tracks_composers | array | Composers with shares; composer_id is the composer’s external ID and share is a string or null |
| tracks_publishers | array | Publishers with shares; publisher_id is the publisher’s external ID and share is a string or null |
| created_at | string | Creation timestamp |
| updated_at | string | Last update timestamp |
| url | string | URL of the track |
Metadata keys are named after the library’s metadata types, so they differ from library to library. A type with no value on the track appears as null or an empty array.
HTTP Response Codes
| Code | Description |
|---|---|
200 OK |
Successful request |
404 Not Found |
Invalid library ID |
POST /api/v1/libraries/{library_id}/tracks
Create a track.
Path Parameters
| Name | Type | Description | Required |
|---|---|---|---|
| library_id | integer | ID of the library | Yes |
Request Body
Wrap the fields in a track key.
| Field | Type | Description | Required |
|---|---|---|---|
| id | integer | Your external ID for the track, unique within the library | Yes |
| title | string | Title of the track (max 255 chars) | Yes |
| description | string | Description of the track (max 1500 chars) | No |
| lyrics | string | Full lyrics (max 5000 chars). Indexed for full-text and semantic search. | No |
| lyrics_chorus_only | string | Chorus lyrics (max 5000 chars) | No |
| active | boolean | Whether the track is active | No |
| instrumental | boolean | Whether the track is instrumental | No |
| explicit | boolean | Whether the track contains explicit content | No |
| clean_available | boolean | Whether a clean version is available | No |
| public_domain | boolean | Whether the track is in the public domain | No |
| bpm | integer | Beats per minute. 0 means unknown and is stored as null. |
No |
| duration | integer | Duration in seconds, greater than zero | No |
| isrc | string | ISRC, two letters, three letters or digits, seven digits. Stored upper case; unique within the library. | No |
| style_alikes | array | Names of artists the track sounds like | No |
| {type}, {type}_id, {type}_ids, {type}_name, {type}_names | mixed | A value for a metadata type (see below) | No |
| tracks_composers | array | Composers with shares (see below) | No |
| tracks_publishers | array | Publishers with shares (see below) | No |
Text longer than a field’s maximum is cut to fit rather than rejected. A key that is not a built-in field or a metadata type is reported as an error.
Blank values. Sending null, an empty string or an empty list removes the value. Optional text fields are stored as null when blank, so “has no description” has exactly one representation and searches can find it with is_null.
Metadata values
Each of the library’s metadata types can be set under its name. The accepted forms depend on the type. Each type’s track_keys in the metadata types response names the exact keys for that type, so a client need not work them out.
Tag types, single-valued:
genre_id: 3sets the tag with external ID 3. An ID the type does not have is ignored.genre_name: "rock"sets the tag by name. On a type without external IDs a tag that does not exist is created. On a type with them the tag must exist already, since a name alone gives a new tag no ID; use the object form below to create one.genre: { "id": 3, "name": "rock" }sets the tag by external ID, creating it with that name if it does not exist. An existing tag is not renamed.genre: 3orgenre: "rock"are treated as an ID or a name respectively.
Tag types that allow multiple values take a list in the same forms: subgenre_ids: [17, 169], subgenre_names: ["blues", "folk"] or subgenres: [{ "id": 17, "name": "blues" }]. The list replaces the track’s previous tags of that type.
Other types take the value directly, under the type’s name: release_date: "2019-04-12", rating: 4.5, cover_song: true. A multi-valued type takes a list under the plural name, which replaces the previous values.
Composers and publishers
tracks_composers and tracks_publishers are lists that replace the track’s previous composers or publishers. Each entry carries a share and refers to the record either by external ID or as an object:
{
"tracks_composers": [
{ "composer_id": 186, "share": 50.0 },
{
"composer": {
"id": 187,
"name": "New Composer",
"pro_id": 21,
"ipi_number": "00000000456"
},
"share": 50.0
}
],
"tracks_publishers": [
{ "publisher": { "id": 2, "name": "Mike's Awesome Music", "pro_id": 21 }, "share": 100.0 }
]
}
A composer_id or publisher_id must already exist in the library. An object form creates the record if its id is new and otherwise refers to the existing record without changing it. A credit sent without a share is stored with none and comes back as "share": null. The fields accepted are the same as the composers and publishers endpoints take.
Example
curl -X POST https://app.spondulix.ai/api/v1/libraries/2/tracks \
-H "Authorization: Bearer your-api-token-here" \
-H "Content-Type: application/json" \
-d '{
"track": {
"id": 1001,
"title": "Highway Windows Down",
"lyrics": "Roll the windows down ...",
"instrumental": false,
"bpm": 118,
"duration": 204,
"genre_name": "indie pop",
"moods": [{ "id": 12, "name": "upbeat" }, { "id": 40, "name": "summer" }],
"style_alikes": ["haim", "best coast"],
"tracks_composers": [{ "composer_id": 186, "share": 100 }]
}
}'
Response
Returns the created track as shown in the list response.
HTTP Response Codes
| Code | Description |
|---|---|
201 Created |
Successful creation |
403 Forbidden |
Your role does not allow editing |
404 Not Found |
Invalid library ID |
409 Conflict |
The track could not be saved because other requests were changing the same records; send it again |
422 Unprocessable Content |
Invalid request (see response body for details) |
A 409 is rare: the save is retried four times first. Its body is { "message": "The database was busy with a conflicting change; try again (Deadlocked)" }, nothing of the track was saved, and sending it again is safe.
A 422 body lists the problems by field, with metadata and composer problems under base:
{
"title": ["can't be blank"],
"base": ["Unknown metadata type: genrre_id", "Composer 187 is invalid: Name can't be blank"]
}
Naming a tag that does not exist on a type with external IDs fails with a message that says what to send instead:
{
"base": ["The genre tag \"ska\" does not exist, and a new genre needs an id: use genres: [{\"id\": ..., \"name\": \"ska\"}]"]
}
POST /api/v1/libraries/{library_id}/tracks/bulk
Create or update up to 200 tracks in one request. Each track is what POST .../tracks takes. A track whose id is already in the library is updated, and one whose id is not is created.
Each track stands or falls alone. A track that fails is reported and leaves nothing behind, not even the tags, composers or publishers it would have created; the others are saved.
Path Parameters
| Name | Type | Description | Required |
|---|---|---|---|
| library_id | integer | ID of the library | Yes |
Request Body
| Field | Type | Description | Required |
|---|---|---|---|
| tracks | array | The tracks, up to 200, each with the fields POST .../tracks takes |
Yes |
{
"tracks": [
{ "id": 1001, "title": "Highway Windows Down", "bpm": 118 },
{ "id": 1002, "title": "" }
]
}
Response
{
"created": 1,
"updated": 0,
"failures": [
{ "index": 1, "id": 1002, "errors": { "title": ["can't be blank"] } }
]
}
Response Fields
| Field | Type | Description |
|---|---|---|
| created | integer | Number of tracks created |
| updated | integer | Number of tracks updated |
| failures | array | The tracks that were not saved; empty when all were |
| index | integer | Place of the track in the list, counting from 0 |
| id | integer | null | The id the track was sent with |
| errors | object | Field name to list of messages, as in a 422 from POST .../tracks. A track that kept losing to other requests has a message under base ending (Deadlocked) or (LockWaitTimeout); nothing of it was saved and sending it again is safe. |
HTTP Response Codes
| Code | Description |
|---|---|
200 OK |
The request was understood. Some tracks may have failed; see failures. |
403 Forbidden |
Your role does not allow editing |
404 Not Found |
Invalid library ID |
422 Unprocessable Content |
tracks is missing, is not a list, or holds more than 200 |
GET /api/v1/libraries/{library_id}/tracks/{id}
Retrieve a track.
Path Parameters
| Name | Type | Description | Required |
|---|---|---|---|
| library_id | integer | ID of the library | Yes |
| id | integer | External ID of the track | Yes |
Response
Returns the track as shown in the list response.
HTTP Response Codes
| Code | Description |
|---|---|
200 OK |
Successful request |
404 Not Found |
Invalid library ID or track ID |
PUT /api/v1/libraries/{library_id}/tracks/{id}
Update a track, or create it if there is no track with that ID, so a loader can send every track the same way without asking first. A new track needs a title, as it does on create, and the response is 201 Created. PATCH works the same way. The body takes the same fields as create; only the fields you send change, so a request with just { "track": { "bpm": 124 } } leaves everything else alone. A list field (style_alikes, tags of a multi-valued type, tracks_composers, tracks_publishers) replaces the whole list when it is sent.
Path Parameters
| Name | Type | Description | Required |
|---|---|---|---|
| library_id | integer | ID of the library | Yes |
| id | integer | External ID of the track | Yes |
Response
Returns the updated or created track.
HTTP Response Codes
| Code | Description |
|---|---|
200 OK |
Successful update |
201 Created |
There was no track with that ID, and one was created |
403 Forbidden |
Your role does not allow editing |
404 Not Found |
Invalid library ID |
409 Conflict |
The track could not be saved because other requests were changing the same records; send it again |
422 Unprocessable Content |
Invalid request (see response body for details) |
DELETE /api/v1/libraries/{library_id}/tracks/{id}
Delete a track, along with its metadata values, lyric index entries and any search results that referenced it.
Path Parameters
| Name | Type | Description | Required |
|---|---|---|---|
| library_id | integer | ID of the library | Yes |
| id | integer | External ID of the track | Yes |
Response
No response body.
HTTP Response Codes
| Code | Description |
|---|---|
204 No Content |
Successful deletion |
403 Forbidden |
Your role does not allow editing |
404 Not Found |
Invalid library ID or track ID |
GET /api/v1/libraries/{library_id}/tracks/{id}/lyric_vectors
Retrieve the track’s lyric chunks and whether each has been embedded. Lyrics are cut into chunks, the whole and each section, and every chunk is embedded in the background through the library’s embedding batches. The vector itself is never returned. Not paged.
Path Parameters
| Name | Type | Description | Required |
|---|---|---|---|
| library_id | integer | ID of the library | Yes |
| id | integer | External ID of the track | Yes |
Response
{
"lyric_vectors": [
{
"id": 169840,
"position": 0,
"chunk_type": "full",
"chunk_text": "1000 feet\n\ni ve got a thousand feet to climb ...",
"chunk_occurrences": 1,
"track_chunk_count": 1,
"chunk_size_share": 1.0,
"embedding_attempts": 0,
"embedding_batch_id": null,
"created_at": "2026-09-16T17:05:25.300-05:00",
"updated_at": "2026-09-16T17:05:25.300-05:00",
"embedded": false
}
]
}
Response Fields
| Field | Type | Description |
|---|---|---|
| id | integer | ID of the chunk |
| position | integer | Place of the chunk among those of its kind |
| chunk_type | string | full for the whole of the lyrics, section_0, section_1 and so on for its sections |
| chunk_text | string | The text, as it is embedded: lower case, without punctuation |
| chunk_occurrences | integer | How many times this text occurs in the lyrics, as a repeated chorus does |
| track_chunk_count | integer | Occurrences of every section of the track, added up |
| chunk_size_share | number | The chunk’s length as a share of the whole lyrics |
| embedding_attempts | integer | Times the chunk has been sent to be embedded |
| embedding_batch_id | integer | null | The batch it is in |
| embedded | boolean | Whether the chunk has been embedded |
| created_at, updated_at | string | Timestamps |
HTTP Response Codes
| Code | Description |
|---|---|
200 OK |
Successful request |
404 Not Found |
Invalid library ID or track ID |