# Recipes: Scarleta API reference

Page: https://www.scarleta.ai/docs/reference/recipes
Base URL: https://api.scarleta.ai. Authentication: `Authorization: Bearer $SCARLETA_API_KEY`.
Operations: 11. Webhooks: 1.

## DELETE /v1/recipe/{recipe_id}

Operation id: `delete_recipe`

**Delete a recipe**

Permanently deletes a recipe saved by your account or team, together with its comments. This cannot be undone. A catalog recipe, another account's recipe, or one already deleted answers `404 not_found`.

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

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `recipe_id` | path | string | yes | The recipe id, for example `rec_4821_mix_check`. Ids are case-insensitive and are read in lower case. Allowed: at least 1 character. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `recipe_id` | string |   |
| `deleted` | true |   |

### Responses

200: Success

```json
{
  "data": {
    "recipe_id": "rec_4821_mix_check",
    "deleted": true
  },
  "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 recipe of yours has this id (a catalog recipe, another account's recipe or one already deleted is answered the same way).

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

429: Over the account request budget for the hour. 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), [`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), [`not_found`](https://www.scarleta.ai/docs/errors#not_found), [`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
RECIPE_ID="YOUR_RECIPE_ID"
curl -X DELETE "https://api.scarleta.ai/v1/recipe/$RECIPE_ID" \
  -H "Authorization: Bearer $SCARLETA_API_KEY"
```

## GET /v1/recipe/{recipe_id}/compare

Operation id: `compare_recipe`

**Compare a fork with its source**

Shows which top-level fields of a fork differ from the recipe it was forked from, and how many steps each has. The recipe in the path must be a fork you can see, and its source must still be visible to you.

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

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `recipe_id` | path | string | yes | The recipe id, for example `rec_4821_mix_check`. Ids are case-insensitive and are read in lower case. Allowed: at least 1 character. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `success` | true | Always true. |
| `fork_id` | string | The fork you asked about. |
| `source_id` | string | The recipe it was forked from. |
| `diff` | object |   |
| `diff.changed` | array<object> | Fields that differ. Empty when the fork is identical. |
| `diff.changed[].field` | string | A recipe field: `recipe_name`, `user_intent`, `domain`, `ai_description`, `steps`, `budget` or `execution`. |
| `diff.changed[].status` | "added" \| "removed" \| "changed" | Whether the fork added the field, removed it or changed it. |
| `diff.changed[].source` | any | The value in the source. |
| `diff.changed[].fork` | any | The value in the fork. |
| `diff.step_count` | object | Steps in each recipe. |
| `diff.step_count.source` | integer | Allowed: -9007199254740991 to 9007199254740991. |
| `diff.step_count.fork` | integer | Allowed: -9007199254740991 to 9007199254740991. |

### Responses

200: Success

```json
{
  "data": {
    "success": true,
    "fork_id": "rec_3791_17925845289883",
    "source_id": "rec_4821_mix_check",
    "diff": {
      "changed": [
        {
          "field": "recipe_name",
          "status": "changed",
          "source": "Tempo check",
          "fork": "Tempo check (fork)"
        }
      ],
      "step_count": {
        "source": 1,
        "fork": 1
      }
    }
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

400: The recipe in the path was not made by forking another.

```json
{
  "error": {
    "code": "not_a_fork",
    "message": "This recipe is not a fork (no forked_from source to compare against)",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#not_a_fork"
  }
}
```

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 recipe with this id is visible to you: it does not exist, or it belongs to another account. Both are answered the same way. When the fork is visible but its source is not, the code is `source_not_found` instead.

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

429: Over the account request budget for the hour. 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), [`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), [`not_a_fork`](https://www.scarleta.ai/docs/errors#not_a_fork), [`not_found`](https://www.scarleta.ai/docs/errors#not_found), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`source_not_found`](https://www.scarleta.ai/docs/errors#source_not_found), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
RECIPE_ID="YOUR_RECIPE_ID"
curl "https://api.scarleta.ai/v1/recipe/$RECIPE_ID/compare" \
  -H "Authorization: Bearer $SCARLETA_API_KEY"
```

## POST /v1/recipe/{recipe_id}/fork

Operation id: `fork_recipe`

**Fork a recipe**

Copies a recipe you can see (a catalog recipe or one of yours) into a new draft that belongs to you, and returns it with `201`. The copy keeps a pointer to the source in `forked_from`; use `GET /v1/recipe/{recipe_id}/compare` to see how a fork has drifted. Without `recipe_name` the copy is named after the source with " (fork)" added. The response also carries internal bookkeeping fields; do not rely on them.

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

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `recipe_id` | path | string | yes | The recipe id, for example `rec_4821_mix_check`. Ids are case-insensitive and are read in lower case. Allowed: at least 1 character. |

### Request body

| Field | Type | Description |
| --- | --- | --- |
| `recipe_name` | string | A name for the copy, at most 200 characters. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `success` | true | Always true. |
| `recipe_id` | string | The id of the new copy. |
| `forked_from` | string | The id of the source. |
| `forked_recipe` | object |   |
| `forked_recipe.recipe_id` | string | Identifier of the recipe. |
| `forked_recipe.recipe_name` | string \| null | The recipe name. |
| `forked_recipe.user_intent` | string | What the recipe is for. |
| `forked_recipe.steps` | array<object> | The ordered steps as stored. |
| `forked_recipe.budget` | any | Budget settings, or null. |
| `forked_recipe.execution` | any | Execution settings, or null. |
| `forked_recipe.tags` | array<string> | Tags on the recipe. |
| `forked_recipe.is_template` | boolean | True for a catalog template recipe. |
| `forked_recipe.usage_count` | integer | How many times the recipe has been run. Allowed: -9007199254740991 to 9007199254740991. |
| `forked_recipe.scope` | string | Who can see the recipe: `org-private` (your team) or `user-custom` (you). |
| `forked_recipe.catalog_status` | string \| null | `draft`, `published`, or null for a recipe saved without a status. |
| `forked_recipe.forked_from` | string \| null | The recipe this one was forked from, or null. |
| `forked_recipe.ai_description` | string \| null | A short description, or null. |
| `forked_recipe.domain` | string \| null | The subject area, or null. |
| `forked_recipe.author_id` | string \| null | Author id for catalog recipes; null for yours. |
| `forked_recipe.author_kind` | string \| null | Author kind for catalog recipes; null for yours. |
| `forked_recipe.created_at` | integer | When the recipe was saved, in milliseconds since the Unix epoch. Allowed: -9007199254740991 to 9007199254740991. |
| `forked_recipe.updated_at` | number \| null | When it was last updated, or null. |
| `forked_recipe.last_used_at` | number \| null | When it was last run, or null. |

### Responses

201: The new draft.

```json
{
  "data": {
    "success": true,
    "recipe_id": "rec_3791_17925845289883",
    "forked_from": "rec_4821_mix_check",
    "forked_recipe": {
      "recipe_id": "rec_3791_17925845289883",
      "recipe_name": "Tempo check (fork)",
      "user_intent": "Estimate the tempo of a track",
      "steps": [
        {
          "step": 1,
          "tool": "tempo_analysis",
          "parameters": {}
        }
      ],
      "budget": null,
      "execution": null,
      "tags": [],
      "is_template": false,
      "usage_count": 0,
      "scope": "user-custom",
      "catalog_status": "draft",
      "forked_from": "rec_4821_mix_check",
      "ai_description": null,
      "domain": null,
      "author_id": null,
      "author_kind": null,
      "created_at": 1792586336723,
      "updated_at": null,
      "last_used_at": null
    }
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

400: `recipe_name` is too long.

```json
{
  "error": {
    "code": "name_too_long",
    "message": "Name must not exceed 200 characters",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#name_too_long"
  }
}
```

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 recipe with this id is visible to you: it does not exist, or it belongs to another account. Both are answered the same way.

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

413: The source recipe is over the size limit.

```json
{
  "error": {
    "code": "recipe_json_too_large",
    "message": "recipe exceeds 102400 bytes (got 102401)",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#recipe_json_too_large"
  }
}
```

429: Over the account request budget for the hour. 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), [`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), [`name_too_long`](https://www.scarleta.ai/docs/errors#name_too_long), [`not_found`](https://www.scarleta.ai/docs/errors#not_found), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`recipe_json_too_large`](https://www.scarleta.ai/docs/errors#recipe_json_too_large), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
RECIPE_ID="YOUR_RECIPE_ID"
curl -X POST "https://api.scarleta.ai/v1/recipe/$RECIPE_ID/fork" \
  -H "Authorization: Bearer $SCARLETA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipe_name": "Tempo check (fork)"
  }'
```

## POST /v1/recipe/{recipe_id}/unpublish

Operation id: `unpublish_recipe`

**Unpublish a recipe**

Moves one of your recipes back to a draft. Only the status changes. You can unpublish recipes saved by your account or team; a catalog recipe or another account's recipe answers `404 not_found`.

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

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `recipe_id` | path | string | yes | The recipe id, for example `rec_4821_mix_check`. Ids are case-insensitive and are read in lower case. Allowed: at least 1 character. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `recipe_id` | string |   |
| `catalog_status` | "draft" |   |

### Responses

200: Success

```json
{
  "data": {
    "recipe_id": "rec_4821_mix_check",
    "catalog_status": "draft"
  },
  "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 recipe of yours has this id (a catalog recipe or another account's recipe is answered the same way).

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

429: Over the account request budget for the hour. 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), [`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), [`not_found`](https://www.scarleta.ai/docs/errors#not_found), [`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
RECIPE_ID="YOUR_RECIPE_ID"
curl -X POST "https://api.scarleta.ai/v1/recipe/$RECIPE_ID/unpublish" \
  -H "Authorization: Bearer $SCARLETA_API_KEY"
```

## POST /v1/recipe/approve

Operation id: `approve_recipe`

**Record an approval or a rejection of a recipe**

Records that you approved or rejected a saved recipe. It changes nothing on the recipe itself and does not run it; it leaves a record that your own tools can use. With `approved: true` you may list `modifications` (fields you changed); their names are echoed in `modifications_applied`. With `approved: false` you may give a `rejection_reason`. `modifications` may be at most 102400 bytes once serialized.

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

### Request body

| Field | Type | Description |
| --- | --- | --- |
| `recipe_id` | string | The id of a recipe in your account, for example `rec_4821_mix_check`. Allowed: at least 1 character. |
| `approved` | boolean | True to approve, false to reject. Must be a JSON boolean. |
| `modifications` | object | Fields you changed before approving. Only used when `approved` is true. |
| `rejection_reason` | string | Why you rejected it. Only kept when `approved` is false. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `success` | true | Always true. |
| `recipe_id` | string | The recipe the decision is about. |
| `request_id` | string | Identifier of this decision record. |
| `approved` | boolean | The decision. |
| `modifications_applied` | array<string> | Names of the fields in `modifications`. Present when approved. |
| `rejection_reason` | string \| null | The reason given, or null. Present when rejected. |

### Responses

200: Success

```json
{
  "data": {
    "success": true,
    "recipe_id": "rec_4821_mix_check",
    "request_id": "0cf07e8d-6f86-47b3-8a35-35499a651571",
    "approved": true,
    "modifications_applied": []
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

400: `recipe_id` is missing, or `approved` is not a JSON boolean, or the body is not valid JSON (`invalid_json`).

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Error: approved (boolean) is required",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_request"
  }
}
```

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 recipe with this id is visible to you.

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

413: `modifications` is over the size limit.

```json
{
  "error": {
    "code": "recipe_json_too_large",
    "message": "modifications exceeds 102400 bytes (got 102401)",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#recipe_json_too_large"
  }
}
```

429: Over the account request budget for the hour. 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), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`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_request`](https://www.scarleta.ai/docs/errors#invalid_request), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`not_found`](https://www.scarleta.ai/docs/errors#not_found), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`recipe_json_too_large`](https://www.scarleta.ai/docs/errors#recipe_json_too_large), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
curl -X POST "https://api.scarleta.ai/v1/recipe/approve" \
  -H "Authorization: Bearer $SCARLETA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipe_id": "rec_4821_mix_check",
    "approved": true
  }'
```

## POST /v1/recipe/cancel

Operation id: `cancel_recipe`

**Set a proposed recipe aside in a conversation**

Declines a recipe that the assistant proposed in one of your conversations; a recipe that was never proposed in the conversation answers `404` `recipe_not_found`. The decline is saved durably in the conversation, and a run of that recipe that is still queued or in progress is stopped. Cancel is silent: it sends no reply and costs nothing. Calling it again for the same conversation and recipe answers `200` too and records nothing new. `reason` is optional. Leading and trailing spaces are removed, a reason that is only spaces counts as none, and only the first 500 characters of the rest are kept.

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

### Request body

| Field | Type | Description |
| --- | --- | --- |
| `recipe_id` | string | The id of the proposed recipe to set aside, at most 200 characters. Only letters, digits, `_`, `.`, `:`, `@` and `-` are allowed; other characters are rejected with 400 `invalid_request`. Allowed: 1 to 200 characters. |
| `conversation_id` | string | The conversation the recipe was proposed in. Must be a UUID of a conversation of yours. |
| `reason` | string | Why you set it aside, in your own words. Only the first 500 characters are kept. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `success` | true | Always true. |
| `recipe_id` | string | The recipe set aside. |
| `conversation_id` | string | The conversation. |
| `declined` | true | Always true. |

### Responses

200: Success

```json
{
  "data": {
    "success": true,
    "recipe_id": "rec_4821_mix_check",
    "conversation_id": "3f9c1d2e-7b44-4c1a-8e0f-5a6b7c8d9e01",
    "declined": true
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

400: `recipe_id` is missing, empty, longer than 200 characters or contains characters outside letters, digits, `_`, `.`, `:`, `@` and `-`, `conversation_id` is missing or not a UUID, or the body is not valid JSON (`invalid_json`).

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Error: a valid conversation_id is required",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_request"
  }
}
```

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 conversation is not yours or does not exist (`not_found`), or the recipe was never proposed in this conversation (`recipe_not_found`).

```json
{
  "error": {
    "code": "not_found",
    "message": "Conversation 3f9c1d2e-7b44-4c1a-8e0f-5a6b7c8d9e01 not found",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#not_found"
  }
}
```

429: Over the account request budget for the hour. 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"
  }
}
```

502: The decline could not be saved. Nothing was cancelled; retry.

```json
{
  "error": {
    "code": "decline_record_failed",
    "message": "Failed to record the cancel. Please try again.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#decline_record_failed"
  }
}
```

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), [`decline_record_failed`](https://www.scarleta.ai/docs/errors#decline_record_failed), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`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_request`](https://www.scarleta.ai/docs/errors#invalid_request), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`not_found`](https://www.scarleta.ai/docs/errors#not_found), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`recipe_not_found`](https://www.scarleta.ai/docs/errors#recipe_not_found), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
curl -X POST "https://api.scarleta.ai/v1/recipe/cancel" \
  -H "Authorization: Bearer $SCARLETA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipe_id": "rec_4821_mix_check",
    "conversation_id": "3f9c1d2e-7b44-4c1a-8e0f-5a6b7c8d9e01",
    "reason": "Too slow."
  }'
```

## POST /v1/recipe/execute

Operation id: `execute_recipe`

**Run a recipe on an audio file**

Starts a recipe on one audio file and answers `200` at once with a `job_id`; read progress with `GET /v1/recipe/jobs/{job_id}` or receive a `recipe_completed` webhook. Name the recipe with `recipe_id` (a saved recipe of yours or a catalog recipe) or send the whole recipe in `recipe`, never both. The cost is estimated before anything starts (a rough run time is shown in `estimated_seconds`, summed from typical timings of each step's tool; a tool with no measured timing counts as 45 seconds); an account whose balance cannot cover it gets `402 insufficient_balance` with the estimate, the balance and the shortfall. `audio_url` must be a public `http` or `https` URL or a file in your own library; audio longer than 60 minutes is refused with 400 `audio_too_long`. Send `conversation_id` of one of your conversations to tie the run to it; repeating the same run for that conversation returns the existing job with `idempotent: true`, and a recipe you set aside with `POST /v1/recipe/cancel` answers `status: declined` with no job. Refusals use the standard error body; `insufficient_balance` (402) carries `estimated_cost`, `balance`, `shortfall` and `audio_duration` in `error.details`, and `audio_too_long` (400) carries `audio_duration` and `max_duration`. The top-level field `budget` is refused; set budgets in the recipe.

Access: API key required; free and paid accounts. Cost: uses tokens. Each step is billed like the matching analysis of the audio: 1 token is 1 second of audio, at the rate of its capability. The estimate is checked against your balance before the run starts..

### Request body

| Field | Type | Description |
| --- | --- | --- |
| `audio_url` | string | The audio file to run the recipe on. Allowed: at least 1 character. |
| `recipe_id` | string | The id of a saved recipe, for example `rec_4821_mix_check`. Send this or `recipe`. |
| `recipe` | object | The whole recipe, as accepted by `POST /v1/recipe/validate`, at most 102400 bytes once serialized. Send this or `recipe_id`. |
| `conversation_id` | string | A conversation of yours to tie the run to. Must be a UUID; a value that is not one is ignored. |
| `webhook_url` | string | Where to POST the `recipe_completed` event. An https URL that resolves to a public address, at most 4096 bytes. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `job_id` | string | Identifier of the run. Absent only when `status` is `declined`. |
| `status` | string | `IN_QUEUE` for a new run; the current status for an existing one; `declined` for a recipe you set aside. |
| `mode` | "recipe" | Always `recipe`. |
| `recipe_id` | string | The recipe that runs. |
| `audio_url` | string | The audio the run uses. |
| `num_steps` | integer | Steps in the recipe. Allowed: -9007199254740991 to 9007199254740991. |
| `estimated_seconds` | integer | A rough run time in seconds. Allowed: -9007199254740991 to 9007199254740991. |
| `idempotent` | boolean | True when this answer repeats an earlier identical run for the same conversation. |
| `conversation_id` | string | Present when `status` is `declined`. |
| `declined` | boolean | True when the recipe was set aside in the conversation; nothing ran. |

### Responses

200: The run was accepted (or an identical earlier run was found).

```json
{
  "data": {
    "job_id": "recipe_9f3a1c7e52b4",
    "status": "IN_QUEUE",
    "mode": "recipe",
    "recipe_id": "rec_4821_mix_check",
    "audio_url": "https://example.com/track-1.wav",
    "num_steps": 1,
    "estimated_seconds": 45
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

400: The body is malformed or incomplete (`invalid_request`, `invalid_json`), the recipe fails validation (`invalid_recipe`, `invalid_recipe_parameters`), a URL is not acceptable (`invalid_audio_url`, `invalid_webhook_url`), or the audio cannot be used (`audio_too_long`, `unsupported_format`, `audio_processing_failed`).

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Provide either recipe_id or recipe.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_request"
  }
}
```

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"
  }
}
```

402: The balance cannot cover the estimate. `error.details` carries `estimated_cost`, `balance`, `shortfall` and `audio_duration`. Nothing was charged.

```json
{
  "error": {
    "code": "insufficient_balance",
    "message": "Running this plan costs about 8 tokens, but your balance is 5. Top up and I will run it.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#insufficient_balance"
  }
}
```

403: The audio file belongs to another account.

```json
{
  "error": {
    "code": "media_not_owned",
    "message": "A file named in the request belongs to a different account.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#media_not_owned"
  }
}
```

404: No recipe with this id is visible to you, or `conversation_id` names a conversation that is not yours. A file at `audio_url` that cannot be downloaded is 404 `audio_not_found`.

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

413: `recipe` (`recipe_json_too_large`) or `webhook_url` (`webhook_url_too_large`) is over its size limit.

```json
{
  "error": {
    "code": "recipe_json_too_large",
    "message": "recipe exceeds 102400 bytes (got 102401)",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#recipe_json_too_large"
  }
}
```

429: Over the account request budget for the hour. 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"
  }
}
```

500: The recipe runner is not configured in this environment. This is a deployment fault, not something to retry.

```json
{
  "error": {
    "code": "workflow_binding_missing",
    "message": "RECIPE_EXECUTE_WORKFLOW binding is not configured.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#workflow_binding_missing"
  }
}
```

502: The run could not be started. Nothing was charged; retry.

```json
{
  "error": {
    "code": "workflow_start_failed",
    "message": "Failed to start recipe execution. Please retry.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#workflow_start_failed"
  }
}
```

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

[`audio_not_found`](https://www.scarleta.ai/docs/errors#audio_not_found), [`audio_processing_failed`](https://www.scarleta.ai/docs/errors#audio_processing_failed), [`audio_too_long`](https://www.scarleta.ai/docs/errors#audio_too_long), [`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`insufficient_balance`](https://www.scarleta.ai/docs/errors#insufficient_balance), [`invalid_audio_url`](https://www.scarleta.ai/docs/errors#invalid_audio_url), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`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_recipe`](https://www.scarleta.ai/docs/errors#invalid_recipe), [`invalid_recipe_parameters`](https://www.scarleta.ai/docs/errors#invalid_recipe_parameters), [`invalid_request`](https://www.scarleta.ai/docs/errors#invalid_request), [`invalid_webhook_url`](https://www.scarleta.ai/docs/errors#invalid_webhook_url), [`media_not_owned`](https://www.scarleta.ai/docs/errors#media_not_owned), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`not_found`](https://www.scarleta.ai/docs/errors#not_found), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`recipe_json_too_large`](https://www.scarleta.ai/docs/errors#recipe_json_too_large), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized), [`unsupported_format`](https://www.scarleta.ai/docs/errors#unsupported_format), [`webhook_url_too_large`](https://www.scarleta.ai/docs/errors#webhook_url_too_large), [`webhook_validation_unavailable`](https://www.scarleta.ai/docs/errors#webhook_validation_unavailable), [`workflow_binding_missing`](https://www.scarleta.ai/docs/errors#workflow_binding_missing), [`workflow_start_failed`](https://www.scarleta.ai/docs/errors#workflow_start_failed)

### Example

```bash
curl -X POST "https://api.scarleta.ai/v1/recipe/execute" \
  -H "Authorization: Bearer $SCARLETA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipe_id": "rec_4821_mix_check",
    "audio_url": "https://example.com/track-1.wav"
  }'
```

## POST /v1/recipe/generate

Operation id: `generate_recipe`

**Generate a recipe from a request**

Describes in words what you want to find out and returns a recipe the planner built for it. Nothing is saved or run: review the recipe, change it if you like, then save it with `POST /v1/recipe/save` and run it. The `recipe_id` in the answer is only a suggestion and is not unique, so save the recipe under your own `rec_` id. The call needs a token balance of at least 1 and does not debit tokens. It calls the planner, so it takes several seconds and can answer `503 planner_unavailable` when the planner times out (the same status as `auth_unavailable`; read the `code`); retry later.

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

### Request body

| Field | Type | Description |
| --- | --- | --- |
| `user_intent` | string | What you want to find out, in words, at most 2000 characters. Allowed: 1 to 2000 characters. |
| `budget_mode` | "fast" \| "balanced" \| "quality" | How much work the plan may use: `fast` for the fewest, cheapest steps, `quality` for the most thorough. |
| `audio_type` | "music" \| "speech" \| "mixed" \| "unknown" | What kind of audio the recipe is for. |
| `max_cost` | number | A cost ceiling in tokens that the planner aims to stay under. |
| `conversation_id` | string | A conversation of yours to attribute the planning to. Must be a UUID. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `success` | true | Always true. |
| `recipe` | object | The recipe. The planner may add fields beyond the ones listed. |
| `recipe.recipe_id` | string | A suggested id. Not unique. |
| `recipe.user_intent` | string | Your request. |
| `recipe.steps` | array<object> | The planned steps. Each has a `step` number, a `tool` name and its `parameters`, and may carry `features`, `label` and `reason`. |
| `recipe.estimated_cost` | number | The planner's estimate of the cost in tokens. |
| `recipe.budget` | object | The budget the plan was made under. |
| `recipe.rationale` | string | A short plain explanation of the plan. |
| `steps_count` | integer | Steps in the recipe. Allowed: -9007199254740991 to 9007199254740991. |
| `estimated_cost` | number | The estimated cost in tokens; 0 when the planner gave none. |

### Responses

200: Success

```json
{
  "data": {
    "success": true,
    "recipe": {
      "recipe_id": "auto_generated_1712700000",
      "user_intent": "Estimate the tempo of my track",
      "steps": [
        {
          "step": 1,
          "tool": "tempo_analysis",
          "parameters": {
            "include_tempo": true
          },
          "features": [
            "tempo.bpm",
            "tempo.beat_count"
          ],
          "label": "Find the tempo"
        }
      ],
      "estimated_cost": 1,
      "budget": {
        "mode": "fast",
        "audio_type": "music",
        "max_cost": 5
      },
      "rationale": "One quick pass to hear the beat and give you the tempo."
    },
    "steps_count": 1,
    "estimated_cost": 1
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

400: `user_intent` is missing, empty or too long, `conversation_id` is not a UUID, the planner could not produce a valid recipe (the message says why), or the body is not valid JSON (`invalid_json`).

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Error: user_intent is required",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_request"
  }
}
```

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"
  }
}
```

402: The account has no token balance, or its tenant is frozen. Nothing was charged.

```json
{
  "error": {
    "code": "insufficient_balance",
    "message": "Your balance is empty. Top up to generate recipes.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#insufficient_balance"
  }
}
```

404: `conversation_id` names a conversation that is not yours or does not exist.

```json
{
  "error": {
    "code": "not_found",
    "message": "Conversation 3f9c1d2e-7b44-4c1a-8e0f-5a6b7c8d9e01 not found",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#not_found"
  }
}
```

429: Over the account request budget for the hour. 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), [`insufficient_balance`](https://www.scarleta.ai/docs/errors#insufficient_balance), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`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_request`](https://www.scarleta.ai/docs/errors#invalid_request), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`not_found`](https://www.scarleta.ai/docs/errors#not_found), [`planner_unavailable`](https://www.scarleta.ai/docs/errors#planner_unavailable), [`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/recipe/generate" \
  -H "Authorization: Bearer $SCARLETA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "user_intent": "Estimate the tempo of my track",
    "budget_mode": "fast"
  }'
```

## GET /v1/recipe/jobs/{job_id}

Operation id: `get_recipe_job`

**Get a recipe run**

Returns the state of a run started with `POST /v1/recipe/execute`. Poll it until `status` is `COMPLETED`, `FAILED`, `CANCELLED` or `TIMEOUT`; `results` is filled in only once the run has ended. Only recipe runs (ids that start with `recipe_`) are read here; use `GET /v1/analyze/{job_id}` for analyses. `completed_steps` and `num_steps` count the steps that finished and the steps in the recipe (`num_steps` is 0 until the run has ended). When you gave a `webhook_url`, `webhook_delivery_status` shows how the `recipe_completed` delivery went: `in_progress`, `delivered`, `failed`, or `unconfirmed`; it is null when there is no webhook or the run has not ended. It describes only that per-request `webhook_url`: addresses registered with `POST /v1/webhooks` are retried the same way, but their outcome is not shown in this field.

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` from the execute answer, for example `recipe_9f3a1c7e52b4`. Allowed: at least 1 character. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `job_id` | string | Identifier of the run. |
| `status` | string \| null | `IN_QUEUE`, `IN_PROGRESS`, `COMPLETED`, `FAILED`, `CANCELLED` or `TIMEOUT`. |
| `mode` | "recipe" | Always `recipe`. |
| `recipe_id` | string \| null | The recipe that ran; null until the run has ended. |
| `audio_url` | string \| null | The audio the run used. |
| `num_steps` | integer | Steps in the recipe; 0 until the run has ended. Allowed: -9007199254740991 to 9007199254740991. |
| `completed_steps` | integer | Steps that finished. Allowed: -9007199254740991 to 9007199254740991. |
| `results` | object \| null | The outcome of the run, once it has ended: the recipe, its steps and their results. Null while the run is queued or in progress, and for `TIMEOUT`. |
| `error` | string \| null | Why the run failed, or null. |
| `error_code` | string \| null | A short code for the failure, or null. |
| `started_at` | number \| null | When the run started, in milliseconds since the Unix epoch, or null. |
| `completed_at` | number \| null | When the run ended, in milliseconds since the Unix epoch, or null. |
| `webhook_delivery_status` | string \| null | State of the `recipe_completed` delivery to the per-request `webhook_url` only (registered addresses are not reflected here), or null. |
| `webhook_last_attempt_at` | number \| null | When the last delivery attempt started, in milliseconds since the Unix epoch, or null. |

### Responses

200: Success

```json
{
  "data": {
    "job_id": "recipe_9f3a1c7e52b4",
    "status": "COMPLETED",
    "mode": "recipe",
    "recipe_id": "rec_4821_mix_check",
    "audio_url": "https://example.com/track-1.wav",
    "num_steps": 1,
    "completed_steps": 1,
    "results": {
      "status": "success",
      "recipe_id": "rec_4821_mix_check",
      "step_count": 1,
      "response": null,
      "conversation_id": null
    },
    "error": null,
    "error_code": null,
    "started_at": 1792586336723,
    "completed_at": 1792586348723,
    "webhook_delivery_status": null,
    "webhook_last_attempt_at": null
  },
  "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 recipe run with this id belongs to your account, or the id does not start with `recipe_`.

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

429: Over the account request budget for the hour. 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), [`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), [`not_found`](https://www.scarleta.ai/docs/errors#not_found), [`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/recipe/jobs/$JOB_ID" \
  -H "Authorization: Bearer $SCARLETA_API_KEY"
```

## POST /v1/recipe/save

Operation id: `save_recipe`

**Save a recipe**

Saves a recipe to your account and returns it. A recipe saved with `recipe_id` of an existing recipe of yours replaces it; one saved under another account's id answers `404 not_found`. Without `catalog_status` or with `published`, the recipe must pass validation and `recipe.recipe_id` is required. With `catalog_status: draft` validation is skipped so you can save work in progress, and a recipe with no `recipe_id` gets one made for you. `catalog_status` can only be `draft` or `published`, and cannot be set on a template recipe (`403 forbidden`). The response also carries internal bookkeeping fields; do not rely on them.

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

### Request body

| Field | Type | Description |
| --- | --- | --- |
| `recipe` | object | The recipe. At most 102400 bytes once serialized. |
| `recipe.recipe_id` | string | The recipe id: letters, digits, underscore and hyphen, at most 128 characters. Required to validate. To save it for later use it must look like `rec_` + four digits + `_` + a name (for example `rec_0001_speech_quality`); a draft save may leave it out. |
| `recipe.user_intent` | string | What the recipe is for, in words. |
| `recipe.steps` | array<object> | The ordered steps, from 1 to 10. |
| `recipe.steps[].step` | integer | The step number, starting at 1. Numbers must be unique. Allowed: -9007199254740991 to 9007199254740991. |
| `recipe.steps[].tool` | string | The capability to run, for example `tempo_analysis`. An unknown name is refused and the error lists the valid ones. |
| `recipe.steps[].parameters` | object | Settings for the step. A plain object; leave it out for none. |
| `recipe.steps[].features` | array<string> | Feature names to compute, where the capability takes them. |
| `recipe.steps[].depends_on` | array<integer> | Earlier step numbers this step needs the output of. |
| `recipe.steps[].timeout_ms` | integer | How long the step may run, in milliseconds, from 1000 to 300000. |
| `recipe.budget` | object | Optional budget settings: `mode` (`fast`, `balanced` or `quality`) and `compute` (`cpu_only` or `gpu_allowed`). |
| `recipe.execution` | object | Optional execution settings. |
| `recipe_name` | string | A name, at most 200 characters. |
| `tags` | array<string> | Tags to keep with the recipe. |
| `is_template` | boolean | Save as a template recipe. Defaults to false. |
| `catalog_status` | "draft" \| "published" | Whether this is a work-in-progress draft or a published recipe. Leave it out for neither. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `success` | true | Always true. |
| `recipe_id` | string | The id the recipe was saved under. |
| `saved_recipe` | object |   |
| `saved_recipe.recipe_id` | string | Identifier of the recipe. |
| `saved_recipe.recipe_name` | string \| null | The recipe name. |
| `saved_recipe.user_intent` | string | What the recipe is for. |
| `saved_recipe.steps` | array<object> | The ordered steps as stored. |
| `saved_recipe.budget` | any | Budget settings, or null. |
| `saved_recipe.execution` | any | Execution settings, or null. |
| `saved_recipe.tags` | array<string> | Tags on the recipe. |
| `saved_recipe.is_template` | boolean | True for a catalog template recipe. |
| `saved_recipe.usage_count` | integer | How many times the recipe has been run. Allowed: -9007199254740991 to 9007199254740991. |
| `saved_recipe.scope` | string | Who can see the recipe: `org-private` (your team) or `user-custom` (you). |
| `saved_recipe.catalog_status` | string \| null | `draft`, `published`, or null for a recipe saved without a status. |
| `saved_recipe.forked_from` | string \| null | The recipe this one was forked from, or null. |
| `saved_recipe.ai_description` | string \| null | A short description, or null. |
| `saved_recipe.domain` | string \| null | The subject area, or null. |
| `saved_recipe.author_id` | string \| null | Author id for catalog recipes; null for yours. |
| `saved_recipe.author_kind` | string \| null | Author kind for catalog recipes; null for yours. |
| `saved_recipe.created_at` | integer | When the recipe was saved, in milliseconds since the Unix epoch. Allowed: -9007199254740991 to 9007199254740991. |
| `saved_recipe.updated_at` | number \| null | When it was last updated, or null. |
| `saved_recipe.last_used_at` | number \| null | When it was last run, or null. |

### Responses

200: Saved.

```json
{
  "data": {
    "success": true,
    "recipe_id": "rec_4821_mix_check",
    "saved_recipe": {
      "recipe_id": "rec_4821_mix_check",
      "recipe_name": "Tempo check",
      "user_intent": "Estimate the tempo of a track",
      "steps": [
        {
          "step": 1,
          "tool": "tempo_analysis",
          "parameters": {}
        }
      ],
      "budget": null,
      "execution": null,
      "tags": [],
      "is_template": false,
      "usage_count": 0,
      "scope": "user-custom",
      "catalog_status": "published",
      "forked_from": null,
      "ai_description": null,
      "domain": null,
      "author_id": null,
      "author_kind": null,
      "created_at": 1792586336723,
      "updated_at": null,
      "last_used_at": null
    }
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

400: The recipe is missing or empty, failed validation, or `catalog_status` is not allowed (`invalid_request`); `recipe_id` has the wrong format (`invalid_recipe_id`); or `recipe_name` is too long (`name_too_long`).

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Cannot save invalid recipe:\n\n- Missing required field: recipe_id",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_request"
  }
}
```

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: `catalog_status` was set on a template recipe.

```json
{
  "error": {
    "code": "forbidden",
    "message": "catalog_status cannot be set on a template recipe; universal publishing is operator-only",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#forbidden"
  }
}
```

404: `recipe.recipe_id` names a recipe that belongs to another account.

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

413: The recipe is over the size limit.

```json
{
  "error": {
    "code": "recipe_json_too_large",
    "message": "recipe exceeds 102400 bytes (got 102401)",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#recipe_json_too_large"
  }
}
```

429: Over the account request budget for the hour. 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), [`forbidden`](https://www.scarleta.ai/docs/errors#forbidden), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`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_recipe_id`](https://www.scarleta.ai/docs/errors#invalid_recipe_id), [`invalid_request`](https://www.scarleta.ai/docs/errors#invalid_request), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`name_too_long`](https://www.scarleta.ai/docs/errors#name_too_long), [`not_found`](https://www.scarleta.ai/docs/errors#not_found), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`recipe_json_too_large`](https://www.scarleta.ai/docs/errors#recipe_json_too_large), [`save_failed`](https://www.scarleta.ai/docs/errors#save_failed), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
curl -X POST "https://api.scarleta.ai/v1/recipe/save" \
  -H "Authorization: Bearer $SCARLETA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipe": {
      "recipe_id": "rec_4821_mix_check",
      "user_intent": "Estimate the tempo of a track",
      "steps": [
        {
          "step": 1,
          "tool": "tempo_analysis",
          "parameters": {}
        }
      ]
    },
    "recipe_name": "Tempo check",
    "catalog_status": "published"
  }'
```

## POST /v1/recipe/validate

Operation id: `validate_recipe`

**Validate a recipe**

Checks a recipe without saving or running it. A valid recipe answers `{ valid: true, steps_count }`. An invalid one answers `400 invalid_request` and the `message` lists every problem, one per line. Run it before saving or running a recipe you built by hand. The recipe may be at most 102400 bytes once serialized.

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

### Request body

| Field | Type | Description |
| --- | --- | --- |
| `recipe` | object |   |
| `recipe.recipe_id` | string | The recipe id: letters, digits, underscore and hyphen, at most 128 characters. Required to validate. To save it for later use it must look like `rec_` + four digits + `_` + a name (for example `rec_0001_speech_quality`); a draft save may leave it out. |
| `recipe.user_intent` | string | What the recipe is for, in words. |
| `recipe.steps` | array<object> | The ordered steps, from 1 to 10. |
| `recipe.steps[].step` | integer | The step number, starting at 1. Numbers must be unique. Allowed: -9007199254740991 to 9007199254740991. |
| `recipe.steps[].tool` | string | The capability to run, for example `tempo_analysis`. An unknown name is refused and the error lists the valid ones. |
| `recipe.steps[].parameters` | object | Settings for the step. A plain object; leave it out for none. |
| `recipe.steps[].features` | array<string> | Feature names to compute, where the capability takes them. |
| `recipe.steps[].depends_on` | array<integer> | Earlier step numbers this step needs the output of. |
| `recipe.steps[].timeout_ms` | integer | How long the step may run, in milliseconds, from 1000 to 300000. |
| `recipe.budget` | object | Optional budget settings: `mode` (`fast`, `balanced` or `quality`) and `compute` (`cpu_only` or `gpu_allowed`). |
| `recipe.execution` | object | Optional execution settings. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `valid` | true |   |
| `steps_count` | integer | Steps in the recipe. Allowed: -9007199254740991 to 9007199254740991. |

### Responses

200: Success

```json
{
  "data": {
    "valid": true,
    "steps_count": 1
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

400: The recipe is missing or not an object (`Error: recipe is required`), or it failed validation (the message lists the problems), or the body is not valid JSON (`invalid_json`).

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Recipe validation failed:\n\n- Step 1: Invalid tool 'unknown_tool'",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_request"
  }
}
```

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"
  }
}
```

413: The recipe is over the size limit.

```json
{
  "error": {
    "code": "recipe_json_too_large",
    "message": "recipe exceeds 102400 bytes (got 102401)",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#recipe_json_too_large"
  }
}
```

429: Over the account request budget for the hour. 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), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`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_request`](https://www.scarleta.ai/docs/errors#invalid_request), [`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), [`recipe_json_too_large`](https://www.scarleta.ai/docs/errors#recipe_json_too_large), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
curl -X POST "https://api.scarleta.ai/v1/recipe/validate" \
  -H "Authorization: Bearer $SCARLETA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipe": {
      "recipe_id": "rec_4821_mix_check",
      "user_intent": "Estimate the tempo of a track",
      "steps": [
        {
          "step": 1,
          "tool": "tempo_analysis",
          "parameters": {}
        }
      ]
    }
  }'
```

## Webhook recipe_completed

**Recipe run completed**

Sent when a run finishes, whether it completed or failed: to the `webhook_url` of the run, if it has one, and to every address registered with `POST /v1/webhooks`. A run that is cancelled, or that stalls and is stopped by Scarleta's automatic recovery, sends no `recipe_completed`; read `GET /v1/recipe/jobs/{job_id}` for its final `status`. The body is a JSON object with exactly the fields below. Each delivery carries `X-Scarleta-Timestamp` (Unix seconds) and `X-Scarleta-Signature` (`v1=` followed by the lowercase hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with your signing secret from `GET /v1/webhooks/signing-secret`). Reject deliveries whose timestamp is more than 300 seconds from your clock. For one day after a rotation a delivery also carries `X-Scarleta-Signature-Previous`, signed with the previous secret; a receiver that has not switched to the new secret yet can verify that header with its old secret. Addresses registered with `POST /v1/webhooks` receive this event too, signed the same way; an address identical to the `webhook_url` is delivered once.

Your endpoint must answer with a 2xx status within 10 seconds. Scarleta makes up to 3 attempts, waiting 2 s and 4 s before the second and third; redirects are not followed. Targets that are not public https endpoints are never called. A run starts one delivery sequence to its `webhook_url` and never a second one, but inside that sequence an attempt that timed out or got a non-2xx answer is sent again with the same body, and the earlier attempt may already have reached you: de-duplicate by `job_id`. Registered addresses are delivered at least once, on a best-effort basis: a failed delivery to a registered address can also be sent again when the same completion is replayed, so de-duplicate by `job_id` there too. `webhook_delivery_status` on the job shows the outcome of the delivery to its `webhook_url` only. Registered addresses are retried the same way, but their outcome is not shown in that field.

Set by the `webhook_url` field of: `execute_recipe`.

### Payload

| Field | Type | Description |
| --- | --- | --- |
| `event` | "recipe_completed" |   |
| `job_id` | string | The run that ended. |
| `recipe_id` | string \| null | The recipe that ran; null if the run ended before the recipe was read. |
| `status` | "completed" \| "failed" | How the run ended. |
| `audio_url` | string | The audio the run used. |
| `num_steps` | integer | Steps in the recipe. Allowed: -9007199254740991 to 9007199254740991. |
| `completed_steps` | integer | Steps that finished. Allowed: -9007199254740991 to 9007199254740991. |
| `error` | string \| null | Why the run failed, or null. |
| `completed_at` | number | When the run ended, in milliseconds since the Unix epoch. |

### Example payload

```json
{
  "event": "recipe_completed",
  "job_id": "recipe_9f3a1c7e52b4",
  "recipe_id": "rec_4821_mix_check",
  "status": "completed",
  "audio_url": "https://example.com/track-1.wav",
  "num_steps": 1,
  "completed_steps": 1,
  "error": null,
  "completed_at": 1792586348723
}
```
