# Moving recipes to v3.1

This page is for integrations that already call the v3 automated recipe operations. It lists what
changes, what stays the same, and in what order to move.

Nothing has to be recreated: recipes deployed through v3 are readable and manageable through v3.1
as they are.

## What changes in the URL

Replace the version segment and keep everything else:

```
/api/v3/automated-recipe*   ->   /api/v3.1/automated-recipe*
```

```bash
curl 'https://embedded.improvado.io/api/v3.1/automated-recipes/' \
  -H "Authorization: Bearer $TOKEN"
```

Authentication does not change. The same bearer token from
[`POST /api/v3/token`](/api/authentication#create-a-token) works on v3.1, and the workspace is
still the one the token was issued for. With cookie authentication, keep sending the workspace id
in the `X-IM-WORKSPACE-ID` header.

**Every path still ends with a slash**, exactly as in v3. A `301` means the slash is missing from
your URL, and most HTTP clients follow that redirect by turning a `POST` into a `GET` and dropping
the body.

Operation by operation:

| v3 | v3.1 |
|---|---|
| [`GET /api/v3/automated-recipe-templates/`](/api/recipes#deprecated-list-automated-recipe-templates) | [`GET /api/v3.1/automated-recipe-templates/`](/api/automated-recipes#list-recipe-templates) |
| [`GET /api/v3/automated-recipe-templates/{template_id}/`](/api/recipes#deprecated-get-automated-recipe-template-detail) | [`GET /api/v3.1/automated-recipe-templates/{template_id}/`](/api/automated-recipes#get-a-recipe-template) |
| [`GET /api/v3/automated-recipes/`](/api/recipes#deprecated-list-automated-recipes) | [`GET /api/v3.1/automated-recipes/`](/api/automated-recipes#list-recipes) |
| [`POST /api/v3/automated-recipes/`](/api/recipes#deprecated-create-automated-recipe) | [`POST /api/v3.1/automated-recipes/`](/api/automated-recipes#create-a-recipe) |
| [`GET /api/v3/automated-recipes/{recipe_id}/`](/api/recipes#deprecated-get-automated-recipe-detail) | [`GET /api/v3.1/automated-recipes/{recipe_id}/`](/api/automated-recipes#get-a-recipe) |
| [`PUT /api/v3/automated-recipes/{recipe_id}/`](/api/recipes#deprecated-update-automated-recipe-configuration) | [`PUT /api/v3.1/automated-recipes/{recipe_id}/`](/api/automated-recipes#configure-a-recipe) |
| [`DELETE /api/v3/automated-recipes/{recipe_id}/`](/api/recipes#deprecated-delete-automated-recipe) | [`DELETE /api/v3.1/automated-recipes/{recipe_id}/`](/api/automated-recipes#delete-a-recipe) |
| [`POST /api/v3/automated-recipes/{recipe_id}/build/`](/api/recipes#deprecated-build-automated-recipe) | [`POST /api/v3.1/automated-recipes/{recipe_id}/build/`](/api/automated-recipes#start-a-recipe-build) |
| [`GET /api/v3/automated-recipes/{recipe_id}/extractions/`](/api/recipes#deprecated-check-the-extraction-tables-a-recipe-reads) | [`GET /api/v3.1/automated-recipes/{recipe_id}/extractions/`](/api/automated-recipes#check-recipe-extraction-tables) |
| [`POST /api/v3/automated-recipes/{recipe_id}/resetup/`](/api/recipes#deprecated-reset-an-automated-recipe-from-its-template) | [`POST /api/v3.1/automated-recipes/{recipe_id}/resetup/`](/api/automated-recipes#recreate-a-recipe-from-its-template) |
| [`GET /api/v3/connections/{connection_id}/accounts`](/api/accounts#list-accounts-for-connection) | [`GET /api/v3.1/connections/{connection_id}/accounts/`](/api/accounts#list-connection-accounts-v3-1) |

The two agency-level recipe operations, `GET /api/v3/recipes/` and
`POST /api/v3/workspaces/{id}/recipes`, are not part of this move.
They are not automated recipe operations, they keep their own authentication, and they stay on v3.

## State values

The `state` of a recipe is now a lower-case machine value instead of a display string:

| v3 | v3.1 |
|---|---|
| `Extracting Data` | `extracting_data` |
| `Building Dependencies` | `building_dependencies` |
| `Ready to work` | `ready_to_work` |
| `Failed` | `failed` |

The new values are used both in responses and in the `?state=` filter of
[`GET /api/v3.1/automated-recipes/`](/api/automated-recipes#list-recipes):

```bash
curl 'https://embedded.improvado.io/api/v3.1/automated-recipes/?state=ready_to_work' \
  -H "Authorization: Bearer $TOKEN"
```

An old value in the filter is rejected:

```json
{
  "code": "invalid_query_parameter",
  "message": "A query parameter has an invalid value.",
  "details": {
    "parameter": "state",
    "allowed_values": ["extracting_data", "building_dependencies", "ready_to_work", "failed"]
  }
}
```

In v3 the same filter with an unknown value returned an empty page, so a client that kept sending
`Ready to work` would quietly see no recipes. In v3.1 it gets a `400` instead.

## What else is different

| In v3 | In v3.1 |
|---|---|
| Errors came in several shapes, and some paths answered with an HTML page | One shape for every error, `{code, message, details}`, always JSON. See [Errors](/errors) |
| A request that contradicted the current state of the recipe answered `400` | `409`, including an attempt to drop a data source, a connection or an account |
| Many failures answered `400` with free-form text and no code | A specific code from the catalogue, or `500 internal_error` |
| The extraction-table check answered `503` when the workspace had no destination | `409 no_active_destination` |
| Create answered `400` when the output table could not be reserved | `503 data_table_reservation_failed` |
| `POST` returned a trimmed object | `201` with the full recipe |
| `PUT` returned `200` with an empty body | `200` with the recipe as it now stands |
| `POST .../build/` returned `200 {status, message}` | `202` with the recipe |
| `POST .../resetup/` returned `200 {status, recipe_id}` | `202` with the recipe |
| `DELETE` returned `204` even when the recipe had not actually been deleted | `500 delete_incomplete` when the recipe itself could not be deleted |
| Starting a build or a recreate on a recipe that was never configured failed later, inside the build | `409 recipe_not_configured` on the spot |
| The extraction-table check answered `{ready, sources}` | `{all_tables_exist, tables}` |
| `last_build_at` held the time the configuration last changed | The time the last build started |
| `datatable_id` and `error` appeared only in the single-recipe response | Both appear in the list as well |
| `error` could be `null` on a failed recipe | `failed` always carries an `error` object |
| An incomplete result looked exactly like a complete one | `degraded`, `not_delivered_reasons` and `sources[]` say what is missing |
| `existing_recipe_ids` came in no particular order | Ordered; the first one is the one to reuse, and the list can be empty |
| A recipe belonging to another workspace answered `403` | `404` |
| `page_size` above 100 was silently capped, and a page past the last one answered `404` | Both are `400 invalid_query_parameter` |
| A template carried nothing to choose it by | `origin`, `categories` and `build_mode`, and the one-recipe-per-template-per-workspace rule is documented |
| `output_columns` could be `null` | An empty list |
| An account was accepted and returned with a second identifier next to `id` | `accounts[]` carries only `id`; no other account identifier is accepted or returned |
| The accounts response carried an `item` object | It does not |
| The accounts path worked with or without the trailing slash | The trailing slash is mandatory |
| A connection from another workspace answered `400` | `404 not_found` |
| `search` on accounts also matched the old account key | Lookup by the old key is not supported |
| `currency` was an empty string when the account had none | `null` |
| The accounts operation documented no paging of its own | 100 accounts per page by default, 100 at most, sorted by `id` |

## Accounts

This is the one place where a straight version swap is not enough. Read it before moving any call
that writes a recipe.

**For a recipe that is already configured**, take the set of accounts from v3.1 itself. Read the
recipe with [`GET /api/v3.1/automated-recipes/{recipe_id}/`](/api/automated-recipes#get-a-recipe),
keep the `connections[]` block exactly as it comes back, append whatever is new, and send the
result to [`PUT`](/api/automated-recipes#configure-a-recipe). The body is the full set, not a
delta, and configuration only ever grows.

```bash
curl 'https://embedded.improvado.io/api/v3.1/automated-recipes/12647075758935484548/' \
  -H "Authorization: Bearer $TOKEN"
```

Do not rebuild that block from the accounts endpoint. Replacing the ids the recipe already holds
is read as removing accounts, and the call is rejected:

```json
{
  "code": "account_removal_not_allowed",
  "message": "Cannot remove accounts [918205] from connection (connection_id=33120, data_source=\"facebook\"). You can only add new accounts.",
  "details": {
    "connections": [
      {
        "connection_id": 33120,
        "connection_data_source": "facebook",
        "accounts": [{"id": 918205}]
      }
    ]
  }
}
```

**For a new recipe, and for accounts you are adding to an existing one**, take every `id` from
[`GET /api/v3.1/connections/{connection_id}/accounts/`](/api/accounts#list-connection-accounts-v3-1)
of the same workspace, and use only accounts with `is_active: true`. Anything else is rejected
with `400 account_unavailable`.

The v3 accounts operation is not a substitute here. For some agencies it returns a different key,
and `PUT` on v3.1 rejects those values with `account_unavailable`.

**If a recipe was already configured with those old keys**, contact support with the recipe id and
the `x-im-trace-id` of a recent request. Do not delete the recipe and deploy it again: deleting a
recipe does not stop the extractions it started, so a fresh recipe would inherit the same data,
and you would have lost the recipe for nothing.

## Moving over one call at a time

There is no cut-over moment and no migration request to send.

- Recipes created through v3 are visible in
  [`GET /api/v3.1/automated-recipes/`](/api/automated-recipes#list-recipes) and can be configured,
  built, recreated and deleted through v3.1 without being recreated.
- The two versions read and write the same recipes, so a client can move one operation at a time
  and keep the rest on v3 while it does.

A workable order:

<Stepper toc={false}>

1. Move the read operations first: the template list, the recipe list and the single recipe. Adapt
   to the new `state` values and to the new error shape while nothing is being written.
2. Move `POST` and `PUT`, switching the source of account ids at the same time, as described above.
3. Move `build`, `resetup`, `extractions` and `DELETE`.
4. Drop the v3 paths from the client.

</Stepper>

:::warning{title="Two behaviours to unlearn"}

v3 retried a failed request up to three times on its own, including create, and it could answer
with an HTML page or a non-JSON body. v3.1 does neither, so retries are yours to decide and every
response body is JSON. See [Errors](/errors) for when a retry is safe.

:::

## How long v3 keeps working

The v3 automated recipe operations keep working until they are switched off. We announce the date
in advance, so there is no need to move in a hurry.

After the switch-off the v3 recipe paths answer `410` for a while, so a client that has not moved
gets an unmistakable status rather than a silent failure. Later the paths are removed.
