
# Apply a configuration plan

`POST /v1/config-plans/{plan_id}:apply` · permission `config:write`, the read permissions of the plan and the write permission of each kind it changes

Applies a stored [plan](create.md) to the environment. Every change is written in one transaction: if one fails, none is kept. A plan can be applied once.

## Request

| Parameter | Description |
|---|---|
| `plan_id` | The plan id, `cfgp_…`. |

The body is optional.

| Field | Description |
|---|---|
| `resolved_secrets` | Object of `secret_ref` name → value for every change with `requires_secret: true` (for example `KEENAUTH_CONNECTION_GITHUB_CLIENT_SECRET`). Values are at most 8 KiB. They are never stored by the plan, logged or returned. |
| `confirm_deletions` | List of `<kind>/<key>` deletions you confirm, for example `connection/legacy`. The plan's `deletion_requires_confirmation` warnings list the deletions that need it. |
| `sp_host` | The environment's service-provider host, for example `login.example.com`. Required only when the plan creates the OIDC material of an enterprise connection (the callback `https://<sp_host>/sso/<connection id>/callback` is fixed from it); a request that does not need it must not send it. |

- The write permissions are `environment-settings:write`, `identity-settings:write`, `attack-protection:write`, `branding:write` (for `branding` and `application_branding`), `connections:write`, `apis:write`, `roles:write` and `applications:write`, only for kinds with a change other than `no_op`.
- Before writing, the target is read again. If any planned resource's version or the content of the plan's kinds changed after the plan was made, nothing is written, the plan becomes `conflicted` and the request fails with `409 config_conflict`. Make a new plan. With `prune`, the kinds that refer to the planned resources are compared too, and the references between resources are checked again; a broken reference also makes the plan `conflicted`.
- Deleting a connection, or an application that received a token in the last 30 days, must be repeated in `confirm_deletions`. Otherwise the request fails with `409 deletion_not_confirmed` and the plan stays `planned`.
- The OIDC material of an enterprise connection (`spec.oidc`) and its client secret are written to the federation service after the connection record. If that second write is refused, the plan stays `planned`, the connection record may already exist, and a new plan writes only the missing material. A `spec.oidc` of `null` leaves the material unchanged. The client secret of a social connection cannot be written yet: a plan with such a `requires_secret` change returns `501 not_implemented` after the secrets are checked. Nothing is written.
- `Idempotency-Key` is supported. Send it without secrets in the body when you can: the key's record keeps a hash of the body.

## Response

`200 OK` with `Cache-Control: no-store`: the plan in the form of [Get a configuration plan](get.md) with `status: applied` and `applied_at`.

The configuration of the affected resources before the change is kept as a revision (the newest 50 per environment, at most 90 days). The apply writes one `config.applied` entry to the environment's management log with the counts of created, updated, deleted and unchanged resources; each changed resource also writes its own entry.

## Errors

| HTTP | code | When |
|---|---|---|
| 400 | `malformed_request`, `unknown_field` | The body is not JSON or has a field other than `resolved_secrets`, `confirm_deletions` and `sp_host`. |
| 401 | `unauthenticated` | The credential is missing, invalid or revoked. |
| 403 | `insufficient_permissions` | The credential lacks a required permission; `required_permissions` lists all of them. |
| 404 | `not_found` | The plan does not exist, belongs to another environment or was deleted. |
| 409 | `config_conflict` | The target changed after the plan was made; the plan is now `conflicted`. |
| 409 | `config_plan_not_applicable` | The plan was already applied or is `conflicted`, or another request is applying it now (`plan_status: applying`); `plan_status` gives its status. |
| 409 | `deletion_not_confirmed` | A protected deletion is missing from `confirm_deletions`; `unconfirmed_deletions` lists them. |
| 409 | `conflict` | Another resource already uses a unique value of a change (`errors[].pointer` is `/changes/<i>/spec/<field>`, code `duplicate`); the plan stays `planned`. |
| 409 | `connection_in_use` | The connection to delete is still in use; the plan stays `planned`. |
| 409 | `connection_test_required` | A change creates an `enterprise` connection `active` or activates one without a successful test sign-in (`errors[].pointer` is `/changes/<i>/spec`, code `test_required`); the plan stays `planned`. |
| 410 | `config_plan_expired` | The plan's hour has passed; make a new plan. |
| 413 | `config_document_too_large` | The changes, or the revision kept for them (at most 10 MiB), are too large to apply in one request; plan fewer kinds. |
| 422 | `validation_failed` | See below. |
| 422 | `policy_conflict` | The change conflicts with an environment policy; the plan stays `planned`. |
| 429 | `rate_limited` | The `config_plan` rate limit (shared with plan creation) was exceeded, or too many plans are being computed or applied for this environment at once; the plan stays `planned`; retry after the `Retry-After` period. |
| 501 | `not_implemented` | The plan needs the client secret of a social connection, which cannot be written yet. |
| 503 | `service_unavailable` | A dependency (the directory or, for enterprise OIDC material, the federation service) is temporarily unavailable or plan capacity is temporarily full; nothing was written or, for the federation service, only the connection record was. Retry after the `Retry-After` period. |

| `errors[].pointer` | `errors[].code` | Meaning |
|---|---|---|
| `/resolved_secrets/<NAME>` | `secret_missing` | The plan needs this secret. |
| `/resolved_secrets/<NAME>` | `secret_unused` | The plan does not need this secret. |
| `/resolved_secrets/<NAME>` | `secret_invalid` | The value is empty, longer than 8 KiB or not text. |
| `/resolved_secrets/<NAME>` | `invalid` | The name does not have the `KEENAUTH_…` form (`A-Z`, `0-9`, `_`). |
| `/sp_host` | `sp_host_required`, `sp_host_unused`, `invalid` | The plan creates OIDC material and no host was given, no host is needed, or the host is not a lowercase DNS name. |
| `/changes/<i>/spec/oidc/...` | as in the connection OIDC API | The federation service rejected the material at this position of the plan's `changes`. |
| `/confirm_deletions/<i>` | `duplicate`, `not_a_deletion` | The item is repeated or is not a deletion in the plan. |
| `/confirm_deletions/<i>` | `invalid` | The item does not have the `<kind>/<key>` form. |
| `/resolved_secrets`, `/confirm_deletions` | `too_many` | More than 5,000 entries. |
| `/changes/<i>/spec/...` | as in the resource's own API | The environment rejected the change at this position of the plan's `changes`. |

## Example

```bash
curl -X POST "https://api.eu.keenauth.com/v1/config-plans/cfgp_01m4qpqtywfs7930tb6kqn5rc5:apply" \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"confirm_deletions": ["connection/legacy"]}'

keenauth config apply cfgp_01m4qpqtywfs7930tb6kqn5rc5 --confirm-deletion connection/legacy
```

`keenauth config apply --secrets-from-env` reads each secret the plan needs from the environment variable of the same name and never prints it.
