
# Create a connection

`POST /v1/connections` · permission `connections:write`

Registers a new connection in the environment. Connections that need protocol configuration (`social`, `enterprise`) start `inactive`; activate them explicitly with `:activate`. An `enterprise` connection cannot be created `active`: a new connection cannot have a successful test sign-in yet. Settings must not contain secrets. An environment can hold at most 100 connections and 20 generic social (`oidc_generic`, `oauth2_generic`) connections, and an organization at most 20 connections.

## Request

JSON body (unknown fields are rejected with `400 unknown_field`):

| Field | Type | Required | Description |
|---|---|---|---|
| `key` | string | yes | Stable configuration key, `^[a-z0-9_-]{1,64}$`, unique in the environment, immutable. |
| `type` | string | yes | `database`, `passwordless`, `passkey`, `social` or `enterprise`. Immutable. |
| `strategy` | string | yes | Strategy for the type, for example `password`, `email_otp`, `google`, `saml`. Immutable. |
| `name` | string | yes | 1-128 characters, unique in the environment. |
| `display_name` | string | no | 1-128 characters shown on sign-in screens. |
| `status` | string | no | `active` or `inactive`. Defaults to `active` for `database`, `passwordless`, `passkey` and to `inactive` for `social`, `enterprise`. |
| `org_id` | string | no | Owner organization (`org_…`), `enterprise` only. Omitted: an environment connection. Immutable. |
| `allowed_email_domains` | array of strings | no | E-mail domains routed to this connection by home realm discovery: IDNA A-label, lower case (for example `xn--bcher-kva.example`), unique, at most 100; non-empty only for `enterprise`. On an organization connection every domain must be a verified domain of the organization, and an `active` organization connection needs at least one. |
| `allow_external_domains` | boolean | no | Also sign in users whose e-mail domain is not listed. `enterprise` only; default `false`. |
| `jit_mode` | string | no | Just-in-time provisioning: `create` creates the user at first sign-in, `create_and_update` also updates the profile from the identity provider, `off` requires an existing account. `enterprise` only. Defaults to `create_and_update` for an environment connection. On an organization connection the mode follows the default when omitted: `create_and_update` while the organization has no active SCIM directory, `off` while it has one (or when the directory state cannot be read); the value is derived on every read, and a mode you set explicitly is not affected. |
| `email_trust` | boolean | no | Trust the e-mail address the identity provider asserts for automatic account linking and just-in-time provisioning. `enterprise` only; default `false`. On an organization connection `true` needs a non-empty `allowed_email_domains` in which every domain is a verified domain of the organization. |
| `default_role_ids` | array of strings | no | Environment roles (`rol_…`, unique, at most 10) granted to the membership a just-in-time sign-in creates, in addition to the environment's default role and the connection's group role mappings. Only on an organization `enterprise` connection. Every role must be a non-privileged environment role of this environment; another environment's role, an organization role, a privileged role or a missing role is rejected. A role deleted or made privileged later is skipped at sign-in. |
| `trust_idp_mfa` | boolean | no | Accept the multi-factor authentication the identity provider asserts (SAML authentication context, OIDC `amr`/`acr`) as the session's MFA proof. `enterprise` only; default `false`. |
| `settings` | object | no | Settings without secrets, up to 32 KiB. Keys or values that look like secrets are rejected. |
| `Idempotency-Key` | header | no | Optional key (16-64 characters) that makes a retried request safe. |

## Response

`201 Created` with the connection and an `ETag` header holding its version.

```json
{
  "id": "conn_0k3v8m1q2x9dz4h7s5t6w0y1ab",
  "key": "google",
  "type": "social",
  "strategy": "google",
  "name": "Google",
  "display_name": "Sign in with Google",
  "status": "inactive",
  "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`.

## Errors

| HTTP | code | When |
|---|---|---|
| 400 | `unknown_field` | The body contains an unknown field. |
| 403 | `quota_exceeded` | A connection limit of the environment or of the organization (`org_id`) is reached. |
| 409 | `conflict` | A connection with the same `key` or `name` exists. |
| 409 | `connection_test_required` | The request creates an `enterprise` connection with `status: active`; nothing is written. Create it `inactive`, test its sign-in, then activate it with `:activate`. |
| 415 | `unsupported_media_type` | The content type is not `application/json`. |
| 422 | `validation_failed` | A field is missing or invalid; `errors[].pointer` names it (for example `/settings/client_secret` for secret-like settings). |
| 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`. |
| 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; retry after the `Retry-After` period. |

## Example

```bash
curl -X POST https://api.eu.keenauth.com/v1/connections \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"google","type":"social","strategy":"google","name":"Google"}'
```
