> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lumos.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate apps and the AppStore

> The v1 equivalent of each v0 app, AppStore, and account-upload call, where each AppStore setting moved, and what takes extra steps on v1.

Most app calls move to v1 with a new path and a few renamed fields. The AppStore is the
exception. Its two v0 settings endpoints become one endpoint with a flat body, and some settings
moved into workflows.

App IDs don't change. The `id` you stored from v0 is the `id` v1 uses in `/v1/apps/{app_uuid}`, so
you don't need to find apps again. Read [What changes in v1](https://docs.lumos.com/reference/v1/migrate/what-changes.md)
first for the conventions every endpoint shares, such as pagination and the error envelope.

## Apps

| v0 | v1 |
| - | - |
| `GET /apps` | [List Apps](https://docs.lumos.com/api-reference/v1/apps/list-apps.md) |
| `GET /apps/{app_id}` | [Get App](https://docs.lumos.com/api-reference/v1/apps/get-app.md) |
| `POST /apps` | [Create App](https://docs.lumos.com/api-reference/v1/apps/create-app.md) |
| `PATCH /apps/{app_id}` | [Update App](https://docs.lumos.com/api-reference/v1/apps/update-app.md) |
| `PUT /apps/{app_id}` (reconnect) | [Update App](https://docs.lumos.com/api-reference/v1/apps/update-app.md), with `auth`, `settings`, and `version` |
| `DELETE /apps/{app_id}` (disconnect) | [Disconnect App](https://docs.lumos.com/api-reference/v1/apps/disconnect-app.md) |
| `POST /apps/{app_id}/sync` | [Trigger App Sync](https://docs.lumos.com/api-reference/v1/apps/trigger-app-sync.md) |
| `GET /apps/categories` | [List App Categories](https://docs.lumos.com/api-reference/v1/app-categories/list-app-categories.md) |

The app object changed in a few places:

* `user_friendly_label` is now `label`. `label`, `category`, `description`, `logo_url`, and
  `website_url` are read-only on v1. The create and update bodies take `label_override` instead of
  `name`, and the matching `*_override` fields for the others, as
  [Display fields](https://docs.lumos.com/reference/v1/objects/app.md#display-fields) describes.
* `sources`, `links`, `request_instructions`, and `allow_multiple_permission_selection` are no
  longer fields on the app object. The last two are AppStore settings now. Create App and Update
  App still accept `request_instructions`.
* v1 adds `approval_workflow`, `default_provisioning_workflow`, `default_deprovisioning_workflow`,
  `appstore_settings`, `post_approval_message`, `sync_status`, and `admins`. `admins` appears only
  with `expand=admins`.

The [App object](https://docs.lumos.com/reference/v1/objects/app.md) page describes every field.

### Values

v1 values are lowercase kebab-case labels, in responses, filters, and request bodies. A v0 value
in a filter or body returns `400`. v0's `request_flow.admins` split admins into `users` and
`groups`. v1's `admins` is one list, and each entry's `type` is `user` or `group`.

| Field | v0 | v1 |
| - | - | - |
| App `status` | `DISCOVERED`, `IN_REVIEW`, `NEEDS_REVIEW`, `APPROVED`, `BLOCKLISTED`, `DEPRECATED` | `discovered`, `in-review`, `needs-review`, `approved`, `blocklisted`, `deprecated` |
| Custom attribute `type` | `TEXT`, `USER`, `SINGLE_SELECT` | `text`, `user`, `single-select` |
| `connection_source` filter on [List Apps](https://docs.lumos.com/api-reference/v1/apps/list-apps.md) | `API`, `UI` | `api`, `ui` |
| `sync_type` on [Trigger App Sync](https://docs.lumos.com/api-reference/v1/apps/trigger-app-sync.md) | `full_sync` | `full-sync` |

```bash v0 theme={null}
curl -s 'https://api.lumos.com/apps?name_search=GitHub&page=1&size=50' \
  --header 'Authorization: Bearer lsk_your_token'
```

```bash v1 theme={null}
curl -s 'https://api.lumos.com/v1/apps?search=GitHub&page_size=50' \
  --header 'Authorization: Bearer lsk_your_token'
```

The list now uses page tokens. Read `apps` and `next_page_token` instead of `items`, `total`, and
`pages`, as [Pagination](https://docs.lumos.com/reference/v1/pagination.md) describes.

### Creating and reconnecting

[Create App](https://docs.lumos.com/api-reference/v1/apps/create-app.md) returns `201`, where v0 returned `200`. It takes one
of two bodies:

* `app_class_id` with `auth`, `settings`, and `version` connects an integration.
* `label_override`, `category_override`, and `description_override` create a custom app.

Sending `auth`, `settings`, or `version` without an integration's `app_class_id` returns `400`.

Reconnecting moved from `PUT /apps/{app_id}` to [Update App](https://docs.lumos.com/api-reference/v1/apps/update-app.md),
the same endpoint that updates the app's `label_override` and `description_override`. `app_class_id` is optional there. Lumos replaces
the credential fields as one group instead of merging them, so send `auth` whenever you send
`settings` or `version`. A body without `auth` returns `400`, and Lumos keeps the stored
credentials.

```bash v0 theme={null}
curl -s -X PUT 'https://api.lumos.com/apps/1a2b3c4d-5e6f-7890-abcd-ef1234567890' \
  --header 'Authorization: Bearer lsk_your_token' \
  --header 'Content-Type: application/json' \
  --data '{
    "app_class_id": "okta.com",
    "auth": { "api_key": "your_okta_api_key" },
    "settings": { "app_instance_identifier": "myorg.okta.com" }
  }'
```

```bash v1 theme={null}
curl -s -X PATCH 'https://api.lumos.com/v1/apps/1a2b3c4d-5e6f-7890-abcd-ef1234567890' \
  --header 'Authorization: Bearer lsk_your_token' \
  --header 'Content-Type: application/json' \
  --data '{
    "auth": { "api_key": "your_okta_api_key" },
    "settings": { "app_instance_identifier": "myorg.okta.com" }
  }'
```

Sending credentials while a sync is running returns `409` with `code` set to `app.sync_in_progress`
and a `Retry-After` header. v0 returned `429`. Sending credentials for an app that wasn't connected
through the API also returns `409`.

### Disconnect versus delete

> [!WARNING]
>
> `DELETE` means something different on v1. `DELETE /apps/{app_id}` disconnected an app on v0.
> `DELETE /v1/apps/{app_uuid}` deletes it, and it removes the credentials in every case.
>
> A custom app's delete returns `204` once the app is gone. If the app still has accounts, or other
> Lumos data still references it, the call returns `409` with `code` set to `app.delete_blocked`,
> and the app stays in a disconnected state. An app connected
> through an integration returns `202` with an operation, and Lumos deletes its record in the
> background. Poll the operation until `done` is `true`, as
> [Long-running operations](https://docs.lumos.com/reference/v1/long-running-operations.md#app-delete) describes.
>
> To disconnect an app as v0's `DELETE` did, call `POST /v1/apps/{app_uuid}:disconnect`. It returns
> the same `id`, `app_class_id`, and `disconnected` fields.

```bash v0 theme={null}
curl -s -X DELETE 'https://api.lumos.com/apps/1a2b3c4d-5e6f-7890-abcd-ef1234567890' \
  --header 'Authorization: Bearer lsk_your_token'
```

```bash v1 theme={null}
curl -s -X POST 'https://api.lumos.com/v1/apps/1a2b3c4d-5e6f-7890-abcd-ef1234567890:disconnect' \
  --header 'Authorization: Bearer lsk_your_token'
```

### Syncs

[Trigger App Sync](https://docs.lumos.com/api-reference/v1/apps/trigger-app-sync.md) takes `{ "sync_type": "full-sync" }`,
where v0 took `full_sync`, and returns an operation instead of `app_id`, `sync_type`, and `status`. Poll the operation, rather than the app,
until `done` is `true`. [Long-running operations](https://docs.lumos.com/reference/v1/long-running-operations.md#app-sync)
describes what it returns. Requesting a sync while another sync is running still returns `409`.

## AppStore

| v0 | v1 |
| - | - |
| `GET /apps/{app_id}/settings`, `GET /appstore/apps/{app_id}/settings` | [Get AppStore Settings](https://docs.lumos.com/api-reference/v1/apps/get-appstore-settings.md) |
| `PATCH /apps/{app_id}/settings`, `PATCH /appstore/apps/{app_id}/settings` | [Update AppStore Settings](https://docs.lumos.com/api-reference/v1/apps/update-appstore-settings.md) |
| `POST /appstore/apps` | [Update AppStore Settings](https://docs.lumos.com/api-reference/v1/apps/update-appstore-settings.md) with `in_app_store: true` |
| `DELETE /appstore/apps/{app_id}` | [Update AppStore Settings](https://docs.lumos.com/api-reference/v1/apps/update-appstore-settings.md) with `in_app_store: false` |
| `GET /appstore/apps` | [Get AppStore Catalog](https://docs.lumos.com/api-reference/v1/apps/get-appstore-catalog.md) for what a requester sees, or [List Apps](https://docs.lumos.com/api-reference/v1/apps/list-apps.md) with `in_app_store=true` for the admin view |
| `GET /appstore/apps/{app_id}` | [Get App](https://docs.lumos.com/api-reference/v1/apps/get-app.md) |

v1's AppStore settings object is flat. v0's `request_flow` and `provisioning` objects are gone,
and each of their fields either sits at the top level or moved elsewhere:

| v0 setting | v1 |
| - | - |
| `in_app_store` | `in_app_store` |
| `custom_request_instructions` | `request_instructions` |
| `redirect_url` | `redirect_url` |
| `request_flow.discoverability`: `FULL`, `LIMITED`, `NONE` | `catalog_visibility`: `visible`, `search-only`, `hidden` |
| `request_flow.allowed_groups` | `allowed_requester_group_uuids`, which takes Lumos group IDs. `null` means everyone (v0's `ALL_GROUPS`) |
| `request_flow.require_manager_approval`, `require_additional_approval`, `approvers`, `approvers_stage_2` | The app's approval workflow: [Get App Approval Workflow](https://docs.lumos.com/api-reference/v1/apps/get-app-approval-workflow.md) and [Set App Approval Workflow](https://docs.lumos.com/api-reference/v1/apps/set-app-approval-workflow.md) |
| `request_flow.custom_approval_message` | `post_approval_message` on the app, set with [Update App](https://docs.lumos.com/api-reference/v1/apps/update-app.md) |
| `request_flow.admins` | `admins` on the app. Read them with `expand=admins` or [Get App Admins](https://docs.lumos.com/api-reference/v1/apps/get-app-admins.md), and set them with [Set App Admins](https://docs.lumos.com/api-reference/v1/apps/set-app-admins.md) |
| `request_flow.request_validation_inline_webhook` | `request_validation_webhook_uuid` |
| `request_flow.response_describes_entire_approval_workflow` | Gone. Read the approval workflow itself |
| `provisioning.groups_provisioning` | `allow_permission_selection`, and each entitlement's visibility. See [Entitlement selection](#entitlement-selection) |
| `provisioning.time_based_access` | `time_based_access.options`. See [Time-based access](#time-based-access) |
| `provisioning.default_time_based_access_option` | `time_based_access.default` |
| `provisioning.allow_multiple_permission_selection` | `allow_multiple_permission_selection` |
| `provisioning.default_permission` | `default_entitlement`, which takes an entitlement ID rather than a requestable-permission ID |
| `provisioning.manual_steps_needed`, `custom_provisioning_instructions` | A manual provisioning task (`manual_provisioning_task`) in the app's default provisioning workflow, bound with [Set App Default Provisioning Workflow](https://docs.lumos.com/api-reference/v1/apps/set-app-default-provisioning-workflow.md) |
| `provisioning.provisioning_webhook` | A `provisioning_webhook` step in the app's default provisioning workflow, with the inline webhook's ID in `provisioning_webhook_id` |
| `provisioning.access_removal_inline_webhook` | A `deprovisioning_webhook` step in the app's default deprovisioning workflow, with the inline webhook's ID in `deprovisioning_webhook_id`, bound with [Set App Default Deprovisioning Workflow](https://docs.lumos.com/api-reference/v1/apps/set-app-default-deprovisioning-workflow.md) |

v1 also adds `custom_intake_fields`, the questions a requester answers on the request form, which
v0's API didn't expose.

### `null` in AppStore settings

> [!WARNING]
>
> [Update AppStore Settings](https://docs.lumos.com/api-reference/v1/apps/update-appstore-settings.md) is a merge patch. A field you omit keeps its value, and Lumos clears
> a field you send as `null`. v0 mostly treated `null` as "leave it alone", so a body built for v0
> that sends `null` for untouched fields erases them on v1. For list fields, such as
> `allowed_requester_group_uuids` and `custom_intake_fields`, Lumos replaces the whole list with
> the value you send.

### Entitlement selection

v1 replaces v0's requestable permissions with entitlements. `groups_provisioning` becomes
`allow_permission_selection`:

* `GROUPS_AND_VISIBLE` is `allow_permission_selection: true`.
* `GROUPS_AND_HIDDEN` is `allow_permission_selection: false`. Requesters can't choose, so a request
  gets the app's `default_entitlement`, or no entitlement when none is set.
* `DIRECT_TO_USER` has no single field. On v0 it hid every requestable permission on the app. On
  v1, set
  `visible_in_appstore` to `false` on each entitlement with
  [Update Entitlement AppStore Settings](https://docs.lumos.com/api-reference/v1/entitlements/update-entitlement-appstore-settings.md).

### Time-based access

`time_based_access.options` is `null` when the app uses your organization's default options. v0
returned the defaults themselves, so v0 can't tell you whether the app had its own list. Setting
`options` pins that list on the app, so later changes to the defaults no longer apply to it. Read
the app on v1 before you write. If `options` is `null`, leave it unset instead of copying v0's
list.

v1 returns each option as a `label` and `value_seconds`. `PATCH` takes labels.

### Adding an app to the AppStore

An app still needs at least one admin, a user or a group, before you can add it to the AppStore.
Set them first with [Set App Admins](https://docs.lumos.com/api-reference/v1/apps/set-app-admins.md). The error for a
missing admin changed from `400` to `409`, with `code` set to `app.admin_required`.

```bash v0 theme={null}
curl -s -X POST 'https://api.lumos.com/appstore/apps' \
  --header 'Authorization: Bearer lsk_your_token' \
  --header 'Content-Type: application/json' \
  --data '{ "app_id": "1a2b3c4d-5e6f-7890-abcd-ef1234567890" }'
```

```bash v1 theme={null}
curl -s -X PATCH 'https://api.lumos.com/v1/apps/1a2b3c4d-5e6f-7890-abcd-ef1234567890/appstore-settings' \
  --header 'Authorization: Bearer lsk_your_token' \
  --header 'Content-Type: application/json' \
  --data '{ "in_app_store": true }'
```

### Finding an app by name

`name_search` is now `search`, a case-insensitive substring match. On [List Apps](https://docs.lumos.com/api-reference/v1/apps/list-apps.md) it matches `label`,
`instance_id`, and `app_class_id`. On
[Get AppStore Catalog](https://docs.lumos.com/api-reference/v1/apps/get-appstore-catalog.md) it matches `label` and `description`. There's no `exact_match`, so to find one app by name,
search and compare `label` yourself.

The catalog handles search-only apps as v0 did. A listing without `search` excludes apps whose
`catalog_visibility` is `search-only`. A call with `search` includes the ones that match.

```bash v0 theme={null}
curl -s 'https://api.lumos.com/appstore/apps?name_search=Slack&exact_match=true' \
  --header 'Authorization: Bearer lsk_your_token'
```

```bash v1 theme={null}
curl -s 'https://api.lumos.com/v1/apps/appstore-catalog?search=Slack' \
  --header 'Authorization: Bearer lsk_your_token'
```

## Account uploads

| v0 | v1 |
| - | - |
| `POST /accounts/upload` | [Upload Accounts](https://docs.lumos.com/api-reference/v1/accounts/upload-accounts.md) |
| `GET /accounts/upload/{job_id}` | [Get Operation](https://docs.lumos.com/api-reference/v1/operations/get-operation.md) |

The body keeps `app_id` and `accounts`. Each account's `permissions` is now `entitlements`, with
the same `unique_identifier`, `type`, and `name` fields. Each account's `status` takes a v1 label,
such as `active` rather than `ACTIVE`. [Lifecycle status](https://docs.lumos.com/reference/v1/objects/account.md#lifecycle-status)
lists them. `accounts` is now required and must hold at least one
account. A request with an unknown field returns `400`.

The response is `202` with an operation rather than `200` with `job_id` and `state`. Poll the
operation until `done` is `true`. Here's how v0's job states map onto the operation:

* `NOT_STARTED` and `RUNNING` are `done: false`.
* `SUCCESS` is `done: true` with a `result`.
* `FAILURE` is `done: true` with an `error` whose `code` is `account_upload.failed`.
* A cancelled upload, which v0 reported as `SUCCESS`, ends with `error.code` set to
  `account_upload.cancelled`.

[Long-running operations](https://docs.lumos.com/reference/v1/long-running-operations.md#account-upload) describes the
operation, and the [Account object](https://docs.lumos.com/reference/v1/objects/account.md#uploading-accounts) page has an
upload example.

## What takes extra steps on v1

Three things v0 did in one call take extra steps on v1:

* **Finding an app by exact name.** Search, then compare `label`, as
  [Finding an app by name](#finding-an-app-by-name) describes.
* **Naming allowed groups by `integration_specific_id`.** v1 takes Lumos group IDs only. Find
  them with [List Groups](https://docs.lumos.com/api-reference/v1/groups/list-groups.md) and its `app_id` and
  `integration_specific_id` filters.
* **Hiding every entitlement on an app** (v0's `DIRECT_TO_USER`). Update each entitlement, as
  [Entitlement selection](#entitlement-selection) describes.

## Terraform

In version 1.x of the Lumos Terraform provider, `lumos_app_store_app` becomes
`lumos_app_appstore_settings`, and its approvers and provisioning steps move into workflows.
`lumos_app` sets display fields through its `*_override` attributes, as v1 does.
[Migrating from 0.x to 1.x](https://registry.terraform.io/providers/teamlumos/lumos/latest/docs/guides/migrating-from-0x)
maps each attribute.
