
# Update a connection

`PATCH /v1/connections/{connection_id}` · permission `connections:write`

Updates a connection with a JSON merge patch (`application/merge-patch+json`, RFC 7396). `If-Match` is required. `key`, `type`, `strategy` and `org_id` cannot be changed. A patch that changes nothing writes nothing and keeps the version. Settings must not contain secrets.

## Request

| Field | Type | Required | Description |
|---|---|---|---|
| `connection_id` | string | yes | Connection identifier (`conn_…`). |
| `If-Match` | header | yes | The `ETag` of the connection as last read. Missing: `428`; stale: `412`. |
| `key` | string | no | Cannot change; the same value is accepted. |
| `type` | string | no | Cannot change. |
| `strategy` | string | no | Cannot change. |
| `name` | string | no | 1-128 characters. |
| `display_name` | string \| null | no | `null` clears it. |
| `status` | string | no | `active` or `inactive`. |
| `org_id` | string \| null | no | Cannot change; the same value is accepted. |
| `allowed_email_domains` | array of strings \| null | no | Replaces the list; `null` or `[]` clears it. Same rules as on create; on an organization connection only newly added domains are checked against the organization's verified domains. |
| `allow_external_domains` | boolean | no | `enterprise` only. |
| `jit_mode` | string | no | `create`, `create_and_update` or `off`. `enterprise` only. |
| `email_trust` | boolean | no | `enterprise` only. Turning it on for an organization connection needs a non-empty `allowed_email_domains` in which every domain is a verified domain of the organization. |
| `default_role_ids` | array of strings \| null | no | Replaces the list; `null` or `[]` clears it. Same rules as on create; only newly added roles are checked, so a role already in the list that was deleted later does not block the update (it is skipped at sign-in). |
| `trust_idp_mfa` | boolean | no | `enterprise` only. |
| `settings` | object \| null | no | Merged recursively (RFC 7396); a `null` member deletes that key; `null` clears all settings. No secrets. |

## Response

`200 OK` with the updated connection and the new `ETag`.

```json
{
  "id": "conn_0k3v8m1q2x9dz4h7s5t6w0y1ab",
  "key": "google",
  "type": "social",
  "strategy": "google",
  "name": "Google",
  "display_name": "Sign in with Google",
  "status": "active",
  "org_id": null,
  "allowed_email_domains": [],
  "allow_external_domains": false,
  "jit_mode": null,
  "email_trust": null,
  "default_role_ids": [],
  "trust_idp_mfa": null,
  "allow_idp_initiated": null,
  "idp_initiated_default_application_id": null,
  "idp_initiated_allowed_application_ids": [],
  "settings": {},
  "created_at": "2026-10-05T09:30:00.123456Z",
  "updated_at": "2026-10-05T09:30:00.123456Z"
}
```

## Passkey settings

For `type: "passkey"` the `settings` object accepts only the keys below. Any other key, a wrong type or an out-of-range value is rejected with `422 validation_failed` and an `errors[].pointer` such as `/settings/max_credentials_per_user`. A `null` value means the default. Shape errors are reported before plan errors.

| Key | Type | Notes |
|---|---|---|
| `rp_id` | string | Absent, `null` or empty: each sign-in host is its own rpId. Otherwise the host name of an `active` custom domain of the environment (lower-case, IDNA A-label); any other value is `422 validation_failed` (`/settings/rp_id`). Once the environment has passkeys, change it with [Change the passkey rpId](change-rp-id.md). |
| `rp_name` | string | Up to 128 characters. |
| `passkey_first_factor_enabled`, `security_key_second_factor_enabled` | boolean | Default `true`. |
| `user_verification` | string | `preferred` (default) or `required`. |
| `max_credentials_per_user` | integer | 1-20, default 20; fractions are rejected. |
| `require_device_bound_passkeys` | boolean | Plan-gated: `true` needs Startup. |
| `attestation_required` | boolean | Plan-gated: `true` needs Enterprise. |
| `aaguid_allowlist`, `aaguid_blocklist` | array of UUID strings | At most 500 unique entries each (errors point at `/settings/<key>/<index>`). Plan-gated: a non-empty list needs Enterprise. |
| `related_origins` | array of strings | `https://host[:port]` origins. Plan-gated: a non-empty list needs Enterprise. |
| `passkey_enrollment_prompt` | string | `off` or `after_login` (default). |

Writing a non-default value of a plan-gated key fails with `403 plan_feature_unavailable` and the members `feature` and `required_plan`. Until plan entitlements are available, these keys cannot be set in any environment; writing a default value (`false`, `[]`, `null`) or removing the key is always allowed. When the feature is available, every `related_origins` entry must be an active verified custom domain of the environment, otherwise the request fails with `422 related_origin_not_verified`. The settings are validated after the merge patch is applied, only when `settings` is part of the patch.

## Errors

| HTTP | code | When |
|---|---|---|
| 400 | `unknown_field` | The patch contains an unknown or read-only field. |
| 404 | `not_found` | The connection does not exist or belongs to another environment. |
| 409 | `connection_test_required` | The patch sets an `enterprise` connection to `active`, or adds a domain to `allowed_email_domains` of an `active` one, without a successful test of the resulting configuration in the last 24 hours (see Activate a connection); nothing is written. Removing domains is not checked. |
| 409 | `rp_id_change_requires_migration` | The patch changes `settings.rp_id` of a passkey connection while the environment has passkeys that would stop working; use [Change the passkey rpId](change-rp-id.md). Nothing is written. |
| 412 | `precondition_failed` | `If-Match` does not match the current version. |
| 415 | `unsupported_media_type` | The content type is not `application/merge-patch+json`. |
| 422 | `validation_failed` | A value is invalid or an immutable field changed (`code: immutable`). |
| 422 | `related_origin_not_verified` | A related origin is not a verified custom domain of the environment. |
| 403 | `plan_feature_unavailable` | The plan does not include a setting you set (`feature`, `required_plan` members), for example `attestation_required`. |
| 428 | `precondition_required` | The `If-Match` header is missing. |
| 401 | `unauthenticated` | The credential is missing, invalid or revoked. |
| 403 | `insufficient_permissions` | The credential lacks the required permission. |
| 429 | `rate_limited` | The rate limit was exceeded; retry after the `Retry-After` period. |
| 503 | `service_unavailable` | A dependency is temporarily unavailable (for example, the passkey check of an `rp_id` change could not run, so nothing is written); retry after the `Retry-After` period. |

## Example

```bash
curl -X PATCH https://api.eu.keenauth.com/v1/connections/conn_0k3v8m1q2x9dz4h7s5t6w0y1ab \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/merge-patch+json" \
  -H 'If-Match: "1"' \
  -d '{"display_name":"Sign in with Google"}'
```
