# Sound grouping: Scarleta API reference

Page: https://www.scarleta.ai/docs/reference/sound-grouping
Base URL: https://api.scarleta.ai. Authentication: `Authorization: Bearer $SCARLETA_API_KEY`.
Operations: 4.

## POST /v1/audio/library/cluster

Operation id: `start_sound_grouping`

**Group your files by how they sound**

Starts a job that sorts your files into groups that sound alike. The answer is 202 at once with the job in state `queued`; read the job (or the latest completed one) until its `status` is `completed`, then use `clusters`. `n_clusters` is a whole number from 2 to 20 (default 8); `filters` narrows which files are grouped (same filters as the library query; default all files); `embedding_model` picks the fingerprint kind, by default the one most of the chosen files have. Only files with a sound fingerprint are grouped. Every check runs before a job is created, so a refused request leaves nothing behind: fewer fingerprinted files than groups is 422 `not_enough_files`, more than 5000 files is 422 `cluster_too_large` (add filters), and a second start while one job is queued or running is 409 `cluster_in_progress` (its `error.details.job_id` names the running job). Sound grouping is for paid accounts: a free key gets 403 `cluster_paid_only`. A job cannot be deleted; completed jobs stay readable. If grouping cannot start right now the route answers 503 `cluster_unavailable`; retry later.

Access: API key required; paid accounts only. Cost: no token cost.

### Request body

| Field | Type | Description |
| --- | --- | --- |
| `n_clusters` | integer | How many groups to make, 2 to 20. Defaults to 8. |
| `filters` | object | Which files to keep. Every field you send narrows the result; an empty object keeps every file. An unknown field is refused with 400 `invalid_filter`. |
| `filters.bpm` | object \| object | Tempo range `{min, max}`, or `{near, tolerance}` (tolerance defaults to 3). |
| `filters.bpm.min` | number |   |
| `filters.bpm.max` | number |   |
| `filters.bpm.near` | number |   |
| `filters.bpm.tolerance` | number |   |
| `filters.tempo_stability` | object |   |
| `filters.tempo_stability.min` | number |   |
| `filters.tempo_stability.max` | number |   |
| `filters.loudness_integrated` | object |   |
| `filters.loudness_integrated.min` | number |   |
| `filters.loudness_integrated.max` | number |   |
| `filters.loudness_range` | object |   |
| `filters.loudness_range.min` | number |   |
| `filters.loudness_range.max` | number |   |
| `filters.true_peak_dbfs` | object |   |
| `filters.true_peak_dbfs.min` | number |   |
| `filters.true_peak_dbfs.max` | number |   |
| `filters.pitch_lowest_midi` | object |   |
| `filters.pitch_lowest_midi.min` | number |   |
| `filters.pitch_lowest_midi.max` | number |   |
| `filters.pitch_highest_midi` | object |   |
| `filters.pitch_highest_midi.min` | number |   |
| `filters.pitch_highest_midi.max` | number |   |
| `filters.notes_per_second` | object |   |
| `filters.notes_per_second.min` | number |   |
| `filters.notes_per_second.max` | number |   |
| `filters.duration_seconds` | object |   |
| `filters.duration_seconds.min` | number |   |
| `filters.duration_seconds.max` | number |   |
| `filters.key_label` | string \| array<string> | One key name or a list, for example `A minor`. |
| `filters.key_root` | string \| array<string> |   |
| `filters.key_mode` | "major" \| "minor" |   |
| `filters.meter` | string \| array<string> | One time signature or a list, for example `4/4`. |
| `filters.speech_language` | string \| array<string> |   |
| `filters.format` | array<string> | File formats to keep, for example `["wav", "mp3"]`. |
| `filters.transcript_contains` | string | Keep files whose transcript contains this text. |
| `filters.sound_description` | string |   |
| `filters.name_contains` | string | Keep files whose name contains this text. |
| `filters.user_tags` | object | Tag rules: keep files with any, with all, or with none of the listed tags. |
| `filters.user_tags.any` | array<string> |   |
| `filters.user_tags.all` | array<string> |   |
| `filters.user_tags.none` | array<string> |   |
| `filters.auto_tags` | array<string> |   |
| `filters.analyzed` | array<string> | Keep files that have results for every listed analysis category. |
| `filters.analyzed_any` | array<string> | Keep files that have results for at least one listed category. |
| `filters.not_analyzed` | boolean | True keeps only files with no analysis results. |
| `filters.missing` | array<string> | Keep files that lack a value, for example `["bpm"]`. |
| `filters.uploaded_after` | number | Milliseconds since the Unix epoch. |
| `filters.uploaded_before` | number | Milliseconds since the Unix epoch. |
| `filters.starred` | boolean |   |
| `filters.archived` | "hide" \| "include" \| "only" | Archived files: hidden (default), included, or the only ones shown. |
| `filters.include_derived` | boolean | True also returns files made from other files, such as stems. |
| `filters.sounds_like` | object | Keep files that sound like the given file. |
| `filters.sounds_like.audio_id` | string |   |
| `filters.sounds_like.min_score` | number |   |
| `filters.sound_group` | object | Keep the files of one sound group from a grouping job. |
| `filters.sound_group.job_id` | string |   |
| `filters.sound_group.group_id` | integer | Allowed: -9007199254740991 to 9007199254740991. |
| `filters.trash` | "hide" \| "include" \| "only" | Trashed files: hidden (default), included, or the only ones shown. |
| `filters.duplicates` | boolean | True keeps only files that have an identical copy. |
| `filters.artist` | array<string> |   |
| `filters.project` | array<string> |   |
| `filters.album` | array<string> |   |
| `filters.title_contains` | string |   |
| `filters.credits_contains` | string |   |
| `embedding_model` | string | Name of the fingerprint kind to compare, as returned in `embedding_model`. Defaults to the kind most chosen files have. Allowed: 1 to 200 characters. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `job_id` | string | Identifier of the new job. |
| `status` | "queued" | Always `queued` at this point. |
| `file_count` | integer | Files the job will group. Allowed: -9007199254740991 to 9007199254740991. |
| `embedding_model` | string | Name of the fingerprint kind that will be compared. |

### Responses

202: The job is queued.

```json
{
  "data": {
    "job_id": "7d2c9e1a-4b3f-4c68-9a5d-1e0f8b7c6a52",
    "status": "queued",
    "file_count": 120,
    "embedding_model": "fingerprint-6144"
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

400: The body is not valid JSON (`invalid_json`), has an unknown field (`invalid_config`), has a bad `n_clusters` (`invalid_n_clusters`), `embedding_model` (`invalid_embedding_model`) or filter (`invalid_filter`).

```json
{
  "error": {
    "code": "invalid_n_clusters",
    "message": "n_clusters must be an integer from 2 to 20",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_n_clusters"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

403: The key belongs to a free account.

```json
{
  "error": {
    "code": "cluster_paid_only",
    "message": "Sound grouping is available on paid plans only. Upgrade to a paid plan to group your library by sound.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#cluster_paid_only"
  }
}
```

409: A job of yours is already queued or running.

```json
{
  "error": {
    "code": "cluster_in_progress",
    "message": "A sound grouping is already running. Wait for it to finish.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#cluster_in_progress"
  }
}
```

422: Too few files with a fingerprint (`not_enough_files`), or more than the limit (`cluster_too_large`).

```json
{
  "error": {
    "code": "not_enough_files",
    "message": "Grouping into 2 groups needs at least 2 files with a sound fingerprint; 0 found.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#not_enough_files"
  }
}
```

429: Over the account request budget or the per-minute cap of this route. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Grouping cannot start right now (`cluster_unavailable`), or the database is briefly unavailable (`db_unavailable`); retry shortly. A key that cannot be checked is `auth_unavailable`.

```json
{
  "error": {
    "code": "cluster_unavailable",
    "message": "Sound grouping is not available right now. Please try again later.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#cluster_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`cluster_in_progress`](https://www.scarleta.ai/docs/errors#cluster_in_progress), [`cluster_paid_only`](https://www.scarleta.ai/docs/errors#cluster_paid_only), [`cluster_too_large`](https://www.scarleta.ai/docs/errors#cluster_too_large), [`cluster_unavailable`](https://www.scarleta.ai/docs/errors#cluster_unavailable), [`db_unavailable`](https://www.scarleta.ai/docs/errors#db_unavailable), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_config`](https://www.scarleta.ai/docs/errors#invalid_config), [`invalid_embedding_model`](https://www.scarleta.ai/docs/errors#invalid_embedding_model), [`invalid_filter`](https://www.scarleta.ai/docs/errors#invalid_filter), [`invalid_json`](https://www.scarleta.ai/docs/errors#invalid_json), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`invalid_n_clusters`](https://www.scarleta.ai/docs/errors#invalid_n_clusters), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`not_enough_files`](https://www.scarleta.ai/docs/errors#not_enough_files), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
curl -X POST "https://api.scarleta.ai/v1/audio/library/cluster" \
  -H "Authorization: Bearer $SCARLETA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "n_clusters": 2,
    "filters": {
      "name_contains": "demo"
    }
  }'
```

## GET /v1/audio/library/cluster/{job_id}

Operation id: `get_sound_grouping`

**Get a sound grouping job**

Returns one grouping job. `clusters` is empty until `status` is `completed`. Files you deleted or trashed since the job ran are left out of every group. A job that stopped making progress is marked `failed` when you read it, with an `error` message. A job that is not yours, does not exist or whose id is not a UUID is 404 `cluster_job_not_found`.

Access: API key required; free and paid accounts. Cost: no token cost.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `job_id` | path | string | yes | The `job_id` returned when the grouping was started. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `job_id` | string | Identifier of the grouping job. |
| `status` | "queued" \| "running" \| "completed" \| "failed" | `queued` and `running` while the job works, then `completed` or `failed`. |
| `n_clusters` | integer | How many groups the job was asked for. Allowed: -9007199254740991 to 9007199254740991. |
| `embedding_model` | string | Name of the fingerprint kind the job compared. |
| `file_count` | integer | Files the job covers. Allowed: -9007199254740991 to 9007199254740991. |
| `filters` | object | The filters the job was started with. |
| `iterations` | integer \| null | Rounds the grouping needed, once it has finished; null before. Allowed: -9007199254740991 to 9007199254740991. |
| `error` | string \| null | Why a failed job failed, or null. |
| `created_at` | number | When the job was created, in milliseconds since the Unix epoch. |
| `updated_at` | number | When the job last changed, in milliseconds since the Unix epoch. |
| `clusters` | array<object> | The groups. Empty until the job is `completed`. |
| `clusters[].cluster_id` | integer | Number of the group inside the job. Use it as `group_id` to name the group. Allowed: -9007199254740991 to 9007199254740991. |
| `clusters[].label` | string \| null | Name you gave the group, or null. |
| `clusters[].file_count` | integer | Files in the group that still exist. Allowed: -9007199254740991 to 9007199254740991. |
| `clusters[].members` | array<string> | `audio_id` of every file in the group, closest to the centre first. |
| `clusters[].representative_files` | array<object> | The files nearest the centre of the group, to show as a preview. |
| `clusters[].representative_files[].audio_id` | string |   |
| `clusters[].representative_files[].name` | string | Display name of the file. |
| `clusters[].centroid_audio_id` | string \| null | The file nearest the centre, or null when the group is empty. |

### Responses

200: The job and its groups.

```json
{
  "data": {
    "job_id": "7d2c9e1a-4b3f-4c68-9a5d-1e0f8b7c6a52",
    "status": "completed",
    "n_clusters": 2,
    "embedding_model": "fingerprint-6144",
    "file_count": 2,
    "filters": {},
    "iterations": 3,
    "error": null,
    "created_at": 1791333870854,
    "updated_at": 1791333874854,
    "clusters": [
      {
        "cluster_id": 0,
        "label": "Drums",
        "file_count": 1,
        "members": [
          "audio_3f9c1a7d2b60"
        ],
        "representative_files": [
          {
            "audio_id": "audio_3f9c1a7d2b60",
            "name": "demo-mix.wav"
          }
        ],
        "centroid_audio_id": "audio_3f9c1a7d2b60"
      },
      {
        "cluster_id": 1,
        "label": null,
        "file_count": 1,
        "members": [
          "audio_a81e5c40d7f2"
        ],
        "representative_files": [
          {
            "audio_id": "audio_a81e5c40d7f2",
            "name": "demo-mix-2.wav"
          }
        ],
        "centroid_audio_id": "audio_a81e5c40d7f2"
      }
    ]
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

404: No grouping job with this id exists in your account (an id that is not a UUID is the same 404).

```json
{
  "error": {
    "code": "cluster_job_not_found",
    "message": "Sound grouping job not found",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#cluster_job_not_found"
  }
}
```

429: Over the account request budget or the per-minute cap of this route. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`cluster_job_not_found`](https://www.scarleta.ai/docs/errors#cluster_job_not_found), [`db_unavailable`](https://www.scarleta.ai/docs/errors#db_unavailable), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
JOB_ID="YOUR_JOB_ID"
curl "https://api.scarleta.ai/v1/audio/library/cluster/$JOB_ID" \
  -H "Authorization: Bearer $SCARLETA_API_KEY"
```

## PUT /v1/audio/library/cluster/{job_id}/groups/{group_id}

Operation id: `name_sound_group`

**Name one group of a sound grouping**

Sets the `label` of one group (`group_id` is the group's `cluster_id`) of a completed job. The label is 1 to 60 characters after trimming, with no control characters. The answer is the whole job. The body and label are checked first (`invalid_json`, `invalid_config` for any field other than `label`, `invalid_label`), then the job (a job that is not yours, does not exist or whose id is not a uuid is 404 `cluster_job_not_found`.), then the group id and the job state, in this order: a `group_id` that is not a whole number is 404 `cluster_group_not_found`, a job that has not completed is 409 `cluster_job_not_ready`, and a number the job has no group for is 404 `cluster_group_not_found`.

Access: API key required; free and paid accounts. Cost: no token cost.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `job_id` | path | string | yes | The `job_id` returned when the grouping was started. |
| `group_id` | path | string | yes | The group's `cluster_id`, a whole number. |

### Request body

| Field | Type | Description |
| --- | --- | --- |
| `label` | string | The new name, 1 to 60 characters; spaces around it are removed first, so the schema limit applies to the trimmed text. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `job_id` | string | Identifier of the grouping job. |
| `status` | "queued" \| "running" \| "completed" \| "failed" | `queued` and `running` while the job works, then `completed` or `failed`. |
| `n_clusters` | integer | How many groups the job was asked for. Allowed: -9007199254740991 to 9007199254740991. |
| `embedding_model` | string | Name of the fingerprint kind the job compared. |
| `file_count` | integer | Files the job covers. Allowed: -9007199254740991 to 9007199254740991. |
| `filters` | object | The filters the job was started with. |
| `iterations` | integer \| null | Rounds the grouping needed, once it has finished; null before. Allowed: -9007199254740991 to 9007199254740991. |
| `error` | string \| null | Why a failed job failed, or null. |
| `created_at` | number | When the job was created, in milliseconds since the Unix epoch. |
| `updated_at` | number | When the job last changed, in milliseconds since the Unix epoch. |
| `clusters` | array<object> | The groups. Empty until the job is `completed`. |
| `clusters[].cluster_id` | integer | Number of the group inside the job. Use it as `group_id` to name the group. Allowed: -9007199254740991 to 9007199254740991. |
| `clusters[].label` | string \| null | Name you gave the group, or null. |
| `clusters[].file_count` | integer | Files in the group that still exist. Allowed: -9007199254740991 to 9007199254740991. |
| `clusters[].members` | array<string> | `audio_id` of every file in the group, closest to the centre first. |
| `clusters[].representative_files` | array<object> | The files nearest the centre of the group, to show as a preview. |
| `clusters[].representative_files[].audio_id` | string |   |
| `clusters[].representative_files[].name` | string | Display name of the file. |
| `clusters[].centroid_audio_id` | string \| null | The file nearest the centre, or null when the group is empty. |

### Responses

200: The job with the new label.

```json
{
  "data": {
    "job_id": "7d2c9e1a-4b3f-4c68-9a5d-1e0f8b7c6a52",
    "status": "completed",
    "n_clusters": 2,
    "embedding_model": "fingerprint-6144",
    "file_count": 2,
    "filters": {},
    "iterations": 3,
    "error": null,
    "created_at": 1791333870854,
    "updated_at": 1791333874854,
    "clusters": [
      {
        "cluster_id": 0,
        "label": "Drums",
        "file_count": 1,
        "members": [
          "audio_3f9c1a7d2b60"
        ],
        "representative_files": [
          {
            "audio_id": "audio_3f9c1a7d2b60",
            "name": "demo-mix.wav"
          }
        ],
        "centroid_audio_id": "audio_3f9c1a7d2b60"
      },
      {
        "cluster_id": 1,
        "label": null,
        "file_count": 1,
        "members": [
          "audio_a81e5c40d7f2"
        ],
        "representative_files": [
          {
            "audio_id": "audio_a81e5c40d7f2",
            "name": "demo-mix-2.wav"
          }
        ],
        "centroid_audio_id": "audio_a81e5c40d7f2"
      }
    ]
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

400: The body is not valid JSON (`invalid_json`), has a field other than `label` (`invalid_config`), or the label is empty, too long or has control characters (`invalid_label`).

```json
{
  "error": {
    "code": "invalid_label",
    "message": "label must be 1 to 60 characters",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_label"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

404: The job does not exist (`cluster_job_not_found`) or has no such group (`cluster_group_not_found`).

```json
{
  "error": {
    "code": "cluster_group_not_found",
    "message": "Sound group not found",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#cluster_group_not_found"
  }
}
```

409: The job is still queued or running, or failed.

```json
{
  "error": {
    "code": "cluster_job_not_ready",
    "message": "This grouping job has not finished yet.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#cluster_job_not_ready"
  }
}
```

429: Over the account request budget or the per-minute cap of this route. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`cluster_group_not_found`](https://www.scarleta.ai/docs/errors#cluster_group_not_found), [`cluster_job_not_found`](https://www.scarleta.ai/docs/errors#cluster_job_not_found), [`cluster_job_not_ready`](https://www.scarleta.ai/docs/errors#cluster_job_not_ready), [`db_unavailable`](https://www.scarleta.ai/docs/errors#db_unavailable), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_config`](https://www.scarleta.ai/docs/errors#invalid_config), [`invalid_json`](https://www.scarleta.ai/docs/errors#invalid_json), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`invalid_label`](https://www.scarleta.ai/docs/errors#invalid_label), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
JOB_ID="YOUR_JOB_ID"
GROUP_ID="YOUR_GROUP_ID"
curl -X PUT "https://api.scarleta.ai/v1/audio/library/cluster/$JOB_ID/groups/$GROUP_ID" \
  -H "Authorization: Bearer $SCARLETA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Drums"
  }'
```

## GET /v1/audio/library/cluster/latest

Operation id: `get_latest_sound_grouping`

**Get your latest finished sound grouping**

Returns your most recent completed grouping job with its groups. Send `embedding_model` to get the latest one of one fingerprint kind. When you have no completed job the answer is 404 `cluster_job_not_found`.

Access: API key required; free and paid accounts. Cost: no token cost.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `embedding_model` | query | string | no | Only consider jobs of this fingerprint kind. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `job_id` | string | Identifier of the grouping job. |
| `status` | "queued" \| "running" \| "completed" \| "failed" | `queued` and `running` while the job works, then `completed` or `failed`. |
| `n_clusters` | integer | How many groups the job was asked for. Allowed: -9007199254740991 to 9007199254740991. |
| `embedding_model` | string | Name of the fingerprint kind the job compared. |
| `file_count` | integer | Files the job covers. Allowed: -9007199254740991 to 9007199254740991. |
| `filters` | object | The filters the job was started with. |
| `iterations` | integer \| null | Rounds the grouping needed, once it has finished; null before. Allowed: -9007199254740991 to 9007199254740991. |
| `error` | string \| null | Why a failed job failed, or null. |
| `created_at` | number | When the job was created, in milliseconds since the Unix epoch. |
| `updated_at` | number | When the job last changed, in milliseconds since the Unix epoch. |
| `clusters` | array<object> | The groups. Empty until the job is `completed`. |
| `clusters[].cluster_id` | integer | Number of the group inside the job. Use it as `group_id` to name the group. Allowed: -9007199254740991 to 9007199254740991. |
| `clusters[].label` | string \| null | Name you gave the group, or null. |
| `clusters[].file_count` | integer | Files in the group that still exist. Allowed: -9007199254740991 to 9007199254740991. |
| `clusters[].members` | array<string> | `audio_id` of every file in the group, closest to the centre first. |
| `clusters[].representative_files` | array<object> | The files nearest the centre of the group, to show as a preview. |
| `clusters[].representative_files[].audio_id` | string |   |
| `clusters[].representative_files[].name` | string | Display name of the file. |
| `clusters[].centroid_audio_id` | string \| null | The file nearest the centre, or null when the group is empty. |

### Responses

200: The latest completed job.

```json
{
  "data": {
    "job_id": "7d2c9e1a-4b3f-4c68-9a5d-1e0f8b7c6a52",
    "status": "completed",
    "n_clusters": 2,
    "embedding_model": "fingerprint-6144",
    "file_count": 2,
    "filters": {},
    "iterations": 3,
    "error": null,
    "created_at": 1791333870854,
    "updated_at": 1791333874854,
    "clusters": [
      {
        "cluster_id": 0,
        "label": "Drums",
        "file_count": 1,
        "members": [
          "audio_3f9c1a7d2b60"
        ],
        "representative_files": [
          {
            "audio_id": "audio_3f9c1a7d2b60",
            "name": "demo-mix.wav"
          }
        ],
        "centroid_audio_id": "audio_3f9c1a7d2b60"
      },
      {
        "cluster_id": 1,
        "label": null,
        "file_count": 1,
        "members": [
          "audio_a81e5c40d7f2"
        ],
        "representative_files": [
          {
            "audio_id": "audio_a81e5c40d7f2",
            "name": "demo-mix-2.wav"
          }
        ],
        "centroid_audio_id": "audio_a81e5c40d7f2"
      }
    ]
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

404: No grouping job with this id exists in your account (an id that is not a UUID is the same 404).

```json
{
  "error": {
    "code": "cluster_job_not_found",
    "message": "Sound grouping job not found",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#cluster_job_not_found"
  }
}
```

429: Over the account request budget or the per-minute cap of this route. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`cluster_job_not_found`](https://www.scarleta.ai/docs/errors#cluster_job_not_found), [`db_unavailable`](https://www.scarleta.ai/docs/errors#db_unavailable), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
curl "https://api.scarleta.ai/v1/audio/library/cluster/latest" \
  -H "Authorization: Bearer $SCARLETA_API_KEY"
```
