---
updatedAt: 2026-07-23T22:25:39.000Z
agentTools:
  projectIndex: https://developers.lumos.com/llms.txt
---

# Managing Integrations via Terraform

The Lumos Terraform provider lets you manage your app integrations as code. You declare each integration as a `lumos_app` resource, then version, review, and apply it like any other infrastructure — instead of clicking through the Lumos UI. This guide is the Terraform counterpart to Managing Integrations via API.

## Overview

A `lumos_app` resource represents a connection between Lumos and one of your apps (for example Okta or Google Workspace). With it you can declare an integration and its credentials in HCL, create it with `terraform apply`, read its state, rotate credentials by re-applying, import an integration that already exists, and remove it with `terraform destroy`. Managing integrations this way gives you repeatable, reviewable, and auditable change control.

## Before you start

You need:

* **Terraform 1.0 or later.**
* **The&#x20;**`teamlumos/lumos`**&#x20;provider** (configured in the next section).
* **A Lumos API token.** Create one in the Lumos UI under **Settings → API Tokens**. It's shown once — copy it somewhere safe. The provider reads it from the `LUMOS_ACCESS_TOKEN` environment variable.
* **Permission to manage integrations.** The token acts as the user who created it, so that user must be an admin with permission to connect and remove integrations. Otherwise applies fail with `HTTP 403`.

Some apps (those that use OAuth or the Merge widget) can't be fully connected from Terraform alone — `apply` starts the connection and you finish authorizing it in the Lumos UI. See [step 6](#6-read-its-state).

## Concept-to-operation map

| I want to...                              | How                                                                                                                    |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Find what an integration needs to connect | The REST discovery endpoints — see [step 1](#1-discover-the-integration-and-its-connection-schema).                    |
| Connect a new integration                 | Declare a `lumos_app` resource, then `terraform apply`.                                                                |
| Set credentials and configuration         | `auth` and `settings` arguments (as `jsonencode({...})`).                                                              |
| Read an integration's state               | The resource's computed attributes, e.g. `disconnected` and `instance_id` (via an `output` or `terraform state show`). |
| Rotate credentials / change fields        | Edit `auth`/`settings` and `terraform apply`.                                                                          |
| Adopt an integration that already exists  | `terraform import`.                                                                                                    |
| Disconnect an integration                 | Remove the resource and `terraform apply`, or `terraform destroy`.                                                     |

## Configure the provider

Declare the provider and authenticate. The token comes from the `LUMOS_ACCESS_TOKEN` environment variable, so you don't commit it to your configuration:

```hcl
terraform {
  required_providers {
    lumos = {
      source  = "teamlumos/lumos"
      version = "~> 0.11"
    }
  }
}

provider "lumos" {
  # Authenticates with the LUMOS_ACCESS_TOKEN environment variable.
  # server_url defaults to https://api.lumos.com.
}
```

```bash
export LUMOS_ACCESS_TOKEN="your-lumos-api-token"
```

To set the token in configuration instead of the environment, use the sensitive `http_bearer` argument: `provider "lumos" { http_bearer = var.lumos_token }`.

## Core workflow

### 1. Discover the integration and its connection schema

Before writing the resource, find the integration's `app_class_id` and check its **connection schema** — the definitive list of which credential keys go in `auth` and which configuration keys go in `settings`. Both are read from the REST API with the same token the provider uses:

```bash
# Find the app_class_id
curl -s "https://api.lumos.com/v1/integrations?query=okta" \
  -H "Authorization: Bearer $LUMOS_ACCESS_TOKEN"

# See what it needs to connect
curl -s "https://api.lumos.com/v1/integrations/okta.com/connection-schema" \
  -H "Authorization: Bearer $LUMOS_ACCESS_TOKEN"
```

For Okta, the schema shows an `api_key` credential in `auth` and the tenant domain as `app_instance_identifier` in `settings`. Other integrations use different keys — some name the tenant differently, some derive it from the credentials and need no setting at all. See the discovery section of [Managing Integrations via API](./managing_integrations_via_api.md) for how to read the schema in detail.

### 2. Declare an integration resource

**Precondition:** the provider is configured (previous section).

Give the resource the app's identifier in `app_class_id`:

```hcl
resource "lumos_app" "okta" {
  app_class_id = "okta.com"
}
```

### 3. Set its fields

Put **all credentials inside&#x20;**`auth`, encoded with `jsonencode({...})`, and non-secret configuration inside `settings` — shaped exactly as the connection schema from step 1 describes. Keep secrets in sensitive variables, not in the resource literally:

```hcl
variable "okta_api_token" {
  type      = string
  sensitive = true
}

resource "lumos_app" "okta" {
  app_class_id = "okta.com"

  auth = jsonencode({
    api_key = var.okta_api_token
  })

  settings = jsonencode({
    app_instance_identifier = "your-org.okta.com"
  })
}
```

See the [argument reference](#argument-reference) for every field.

### 4. Initialize, plan, and apply

**Precondition:** `LUMOS_ACCESS_TOKEN` is exported.

```bash
terraform init    # downloads the provider
terraform plan    # previews the change
terraform apply   # creates the integration
```

When the credentials are complete, `apply` connects the integration and Lumos starts the first sync automatically.

### 5. Trigger a sync

Lumos starts a sync automatically whenever an `apply` connects or reconnects an integration, so your first `apply` (step 4) already triggered one.

The provider manages connect/reconnect/disconnect — it does not manage syncs. To run a sync on demand afterwards (for example from a CI step after `apply`), call the REST sync endpoint directly with the resource's `id`. Read the id from state with `terraform state show lumos_app.okta`, or expose it with an `output`, then `POST` to `/v1/apps/{id}/sync`:

```bash
curl -s -X POST https://api.lumos.com/v1/apps/$INTEGRATION_ID/sync \
  -H "Authorization: Bearer $LUMOS_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "sync_type": "full_sync" }'
```

A `202 Accepted` confirms the sync was launched (it runs asynchronously). Only one sync runs at a time — if a sync is already in progress the call returns `409`; wait for the running sync to finish, then retry. See the [REST API guide](./managing_integrations_via_api.md) for the full contract.

### 6. Read its state

The resource's computed attributes are managed by Lumos — read them from state rather than setting them. The two most useful are `disconnected` (whether the integration's credentials have been removed) and `instance_id` (the tenant/instance Lumos resolved for the connection). Expose them with an `output`:

```hcl
output "okta_connected" {
  value = !lumos_app.okta.disconnected
}
```

```bash
terraform apply                      # refreshes computed attributes
terraform output okta_connected      # e.g. true
terraform state show lumos_app.okta  # full attribute set
```

For an OAuth or Merge app, `apply` starts the connection but the authorization is finished in the Lumos UI — until you complete it there, the integration isn't syncing yet. Complete the consent in the Lumos UI, and manage the resource from Terraform as usual afterwards:

```hcl
resource "lumos_app" "slack" {
  app_class_id = "slack.com"

  auth = jsonencode({
    client_id     = var.slack_client_id
    client_secret = var.slack_client_secret
  })
  # apply starts the connection; complete OAuth consent in the Lumos UI.
}
```

### 7. Update an integration

**Precondition:** the integration already exists in state.

Change `auth` (for example to rotate an API key) or `settings`, then `apply`:

```hcl
resource "lumos_app" "okta" {
  app_class_id = "okta.com"

  auth = jsonencode({
    api_key = var.okta_api_token_rotated
  })
}
```

```bash
terraform apply
```

Changing `app_class_id` replaces the integration (Terraform destroys and recreates it). If a sync is already running for the integration, the update is throttled with `HTTP 429` and a `Retry-After` hint — wait and re-apply.

### 8. Import an existing integration

**Precondition:** declare a matching `lumos_app` resource block first. You need the integration's `id` (the UUID returned when you connect it via the [Integrations API](./managing_integrations_via_api.md), or listed by `GET /v1/apps?connection_source=API`).

```bash
terraform import lumos_app.okta 7d3b1c2e-8f4a-4c1d-9b2e-1a2b3c4d5e6f
```

Then run `terraform plan` to confirm your HCL matches the imported state, and fill in `auth`/`settings` as needed.

### 9. Destroy it

Removing the resource (or running `terraform destroy`) disconnects the integration and removes its stored credentials. The app remains available to reconnect later.

```bash
terraform destroy -target=lumos_app.okta
```

## Argument reference

These are the arguments you set when declaring an integration. Their meanings match the field reference in *Managing Integrations via API*; discover the exact keys each integration accepts via its connection schema ([step 1](#1-discover-the-integration-and-its-connection-schema)).

| Argument       | What it is                                                                                                                             | Required | Constraints                                                                                                                                                                                                                               |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `app_class_id` | Identifier of the app to connect, e.g. `okta.com`.                                                                                     | Required | Must be an app available to your domain. Changing it **forces replacement** of the resource.                                                                                                                                              |
| `auth`         | The app's credentials as a JSON object, written with `jsonencode({...})`. **All secrets go here.**                                     | Optional | Keys depend on the app (for example `api_key`, or `client_id` and `client_secret`); validated against the app's connection schema. Mark the variables you feed it as `sensitive`. May be empty for apps you finish authorizing in the UI. |
| `settings`     | Non-secret configuration as a JSON object, written with `jsonencode({...})` (for example region, host, or tenant/instance identifier). | Optional | Keys depend on the app; the connection schema lists them. Apps whose tenant is set manually take it here; others derive it from the credentials.                                                                                          |
| `version`      | Pins the integration to a specific version.                                                                                            | Optional | Leave unset to use the default.                                                                                                                                                                                                           |

### Read-only attributes

Lumos computes these; reference them but don't set them:

* `id` — the integration's UUID (use it with `terraform import` and the REST sync endpoint).
* `instance_id` — the tenant/instance Lumos resolved for the connection (either the value you configured in `settings` or one derived from the credentials).
* `disconnected` — `false` while the integration's credentials are stored (connected); `true` after a disconnect.

## Handling errors

The provider surfaces API failures as Terraform diagnostics during `plan` or `apply`, formatted `HTTP <code>: <detail>`. Common ones:

| Failure                                   | Cause                                                                                          | What to do                                                                               |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `Missing Provider Security Configuration` | No token.                                                                                      | Export `LUMOS_ACCESS_TOKEN` (or set `http_bearer`).                                      |
| `HTTP 400`                                | Missing or invalid credentials, an unknown `app_class_id`, or a field the app doesn't support. | Fix `auth`/`app_class_id`; confirm the app's required keys via its connection schema.    |
| `HTTP 401`                                | Invalid API token.                                                                             | Check `LUMOS_ACCESS_TOKEN`.                                                              |
| `HTTP 403`                                | The token's user can't manage integrations.                                                    | Use a token for an admin with integration permissions.                                   |
| `HTTP 404`                                | The integration no longer exists.                                                              | On refresh, Terraform drops it from state — re-`apply` to recreate, or remove the block. |
| `HTTP 409`                                | An integration for this app instance already exists.                                           | `terraform import` it instead of creating a new one.                                     |
| `HTTP 429`                                | A sync is already running for this integration.                                                | Wait for the `Retry-After` interval, then re-apply.                                      |
| `HTTP 502`                                | The app rejected the credentials, or the sync failed.                                          | Verify the credentials with the provider, then re-apply.                                 |

## Next steps

* Expose `disconnected` (and other attributes) as outputs to monitor your integrations.
* Rotate credentials on a schedule by updating `auth` and re-applying — ideally from CI.
* For the underlying field semantics and the discovery endpoints, see [Managing Integrations via API](./managing_integrations_via_api.md) and the full reference at <https://developers.lumos.com>.

<br />