Plan a configuration change
POST /v1/config-plans · permission config:read and the read permission of each kind in the document
Compares a configuration document with the environment's current configuration and stores the result as a plan. Nothing is changed: a plan only shows what applying the document would do. Use it to review a promotion from development to staging or production before you apply it.
Request
| Field | Description |
|---|---|
document |
Required. A configuration document in the format of GET /v1/config-export. It must match the JSON Schema https://docs.keenauth.com/config/schema/v1.json. |
prune |
Optional, default false. When true, resources of the document's kinds that are missing from the document are planned for deletion. |
- The document's kinds are
kindsif given (every resource must then be of a listed kind), otherwise the kinds of its resources. Only these kinds are compared; other kinds are left as they are. - A resource missing from the document is not deleted unless
pruneistrue. The singleton kinds (environment_settings,identity_settings,branding) are never deleted. - In the singleton kinds, a field missing from the document is left unchanged: the target's current value is kept. A document exported before a field existed therefore does not reset that field. A value given explicitly, including
null, is still written. In the other kinds, a missing field takes its default value. - The document must not contain secret values. A secret field holds only a
secret_refplaceholder ornull; a value is rejected and is never echoed back. warningsin the document are ignored; at most 15,000 are accepted (an export never contains more).- The permissions are those of an export of the same kinds. With
prune, the kinds that may refer to a deleted resource are read too, so their permissions are required as well.
Response
201 Created with Location: /v1/config-plans/{plan_id} and Cache-Control: no-store.
| Field | Description |
|---|---|
id |
Plan id, cfgp_…. |
status |
planned; expired after expires_at. |
expires_at |
The plan is valid for 1 hour. It is deleted 7 days after it expires. |
summary |
Number of create, update, delete and no_op actions. |
changes |
One entry per resource, in kind order and then by key. |
warnings |
secret_required: the connection needs a client secret that the target does not have yet. |
Each change has kind, key, action (create, update, delete or no_op), object_version (the target resource's version at plan time; null for create), requires_secret, fields and, for create and update, the desired spec. Each entry in fields is a field difference: pointer (RFC 6901, for example /spec/name), op (add, remove or replace, as in RFC 6902), before and after. Lists are compared as whole values.
An exported document applied unchanged plans no_op for every resource, whatever the order of resources and set-like lists.
{
"id": "cfgp_01m4qpqtywfs7930tb6kqn5rc5",
"status": "planned",
"prune": false,
"kinds": ["connection"],
"expires_at": "2026-10-12T11:00:00Z",
"summary": {"create": 1, "update": 1, "delete": 0, "no_op": 1},
"changes": [
{"kind": "connection", "key": "db", "action": "no_op", "object_version": 5, "requires_secret": false, "fields": []},
{"kind": "connection", "key": "github", "action": "create", "object_version": null, "requires_secret": true, "fields": [], "spec": {"type": "social", "strategy": "github", "name": "GitHub", "display_name": null, "status": "active", "settings": {}, "client_secret": {"secret_ref": {"source": "env", "name": "KEENAUTH_CONNECTION_GITHUB_CLIENT_SECRET"}}}},
{"kind": "connection", "key": "google", "action": "update", "object_version": 4, "requires_secret": false, "fields": [{"pointer": "/spec/name", "op": "replace", "before": "Google", "after": "Google Workspace"}], "spec": {"type": "social", "strategy": "google", "name": "Google Workspace", "display_name": null, "status": "active", "settings": {}, "client_secret": {"secret_ref": {"source": "env", "name": "KEENAUTH_CONNECTION_GOOGLE_CLIENT_SECRET"}}}}
],
"warnings": [{"code": "secret_required", "kind": "connection", "key": "github", "pointer": "/spec/client_secret"}]
}
Every plan writes a config_plan.created entry to the environment's management log.
Limits
- Rate: plans have their own per-minute limit per environment: 20 on Free, 60 on Startup and Business, 120 on Enterprise in
production; one tenth (at least 2) indevelopmentandstaging. - Quota per environment: at most 100 plans that are neither applied nor expired, and at most 2,000 plans and 1 GiB of stored plans until they are deleted (7 days after expiry). Plans of other environments never count.
- Capacity: the server computes a limited number of large plans at the same time; one environment can use at most a quarter of that capacity.
- Field limits are those the configuration service enforces when it writes: the schema bounds lists and texts (for example
redirect_uris50,allowed_web_origins20,permissions1,000), connectionsettingsis at most 32 KiB and 8 levels deep, applicationjwksand each permission'sconsent_text_i18nat most 16 KiB.
Errors
| HTTP | code | When |
|---|---|---|
| 400 | malformed_request, unknown_field |
The body is not JSON or has a field other than document and prune. |
| 401 | unauthenticated |
The credential is missing, invalid or revoked. |
| 403 | insufficient_permissions |
The credential lacks a required permission; required_permissions lists all of them. |
| 413 | config_document_too_large, payload_too_large |
The document has more than 5,000 resources or 10 MiB; plan fewer kinds. |
| 422 | validation_failed |
See below; errors[].pointer points into the request (/document/...). |
| 429 | rate_limited |
The plan rate limit or the environment's plan quota was reached, or the environment is computing too many plans; retry after the Retry-After period (for the quota, when the oldest plan expires or is deleted). |
| 503 | service_unavailable |
Plan computation is at capacity or a dependency is temporarily unavailable; retry after the Retry-After period. |
errors[].code |
Meaning |
|---|---|
required |
document is missing. |
invalid |
The document does not match the schema at this pointer. |
duplicate |
Two resources have the same kind and key. |
kind_not_listed |
The resource's kind is not in kinds. |
secret_value |
A secret value was found; use a secret_ref placeholder. |
unresolved_reference |
The key it refers to would not exist after the change. |
key_in_use |
The key belongs to an application that is not managed by configuration (organization-owned or self-registered), or to the system API. |
still_referenced |
/prune: a resource that stays refers to a resource the plan would delete. |
too_large |
A resource, a warning or a top-level field has more than 200,000 JSON values. |
too_long |
Connection settings exceeds 32 KiB, or application jwks or a permission's consent_text_i18n exceeds 16 KiB. |
too_deep |
Connection settings is nested deeper than 8 levels. |
reserved |
The API identifier is in the platform's urn:idp: namespace. |
Example
curl -X POST "https://api.eu.keenauth.com/v1/config-plans" \
-H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
-H "Content-Type: application/json" \
-d "{\"document\": $(cat keenauth.json), \"prune\": false}"
keenauth config plan -f keenauth.yaml