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.
{
"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. |
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
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"}'