
# Create an application

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

Creates an application (an OAuth/OIDC client) in the environment of the calling key. `key`, `name`, `app_type` and `party` are required; every other field gets a safe default derived from the application type. The response carries an `ETag` that you send as `If-Match` when updating or deleting. This operation does not create a client secret; secrets and keys are managed through the application credential endpoints.

## Request

**Body** (`application/json`). Unknown fields are rejected. Only `key`, `name`, `app_type` and `party` are required.

| Field | Type | Required | Description |
|---|---|---|---|
| `key` | string | yes | Stable configuration key, `^[a-z0-9_-]{1,64}$`, unique per environment. Required on create; cannot change afterwards. |
| `name` | string | yes | Display name, 1-100 characters. |
| `app_type` | string | yes | `web`, `spa`, `native`, `m2m`, `device` or `management`. Required on create; cannot change afterwards. |
| `party` | string | yes | `first_party` or `third_party`. Required on create; can only change from `first_party` to `third_party`. |
| `owner_org_id` | string or null | no | `org_…` id of the owning organization; only for `m2m` applications; cannot change. |
| `status` | string | no | `active` or `suspended`. A suspended application cannot get new tokens. |
| `allowed_web_origins` | string[] | no | Allowed CORS origins (`https://host[:port]`), at most 20, no wildcards. |
| `organization_usage` | string | no | `deny`, `allow` or `require`. Governs the `organization` authorization parameter (an `org_…` id or the organization's slug): `deny` refuses it, `allow` makes it optional, `require` makes it mandatory. The token carries `org_id` only after the user's membership is verified. |
| `org_access_mode` | string | no | Organization sign-in access: `all_organizations` (default; every member of every organization), `enabled_organizations` (only organizations where the application is enabled) or `assigned_only` (additionally only assigned memberships or directory groups). See [organization applications](../organization-applications/get.md). |
| `registration_mode` | string or null | no | `open`, `invite_only` or `closed`; empty inherits the environment setting. |
| `registration_source` | string | no | `dashboard` or `management_api` on create (default `management_api`); cannot change. Responses may also show `dcr` or `cimd`. |
| `verification_status` | string | no | `verified` or `unverified`. |
| `security_profile` | string | no | `baseline` (the only writable profile today). |
| `token_endpoint_auth_method` | string | no | `private_key_jwt`, `client_secret_basic`, `client_secret_post` or `none`; must be allowed for the `app_type`. |
| `grant_types` | string[] | no | Allowed grant types; a subset of what the `app_type` permits. |
| `default_api_id` | string or null | no | `api_…` id of the API used when a request has no `resource`. |
| `require_par` | boolean | no | Require pushed authorization requests. |
| `require_state` | boolean | no | Require `state`. |
| `require_nonce` | boolean | no | Require `nonce`. |
| `pkce_required` | boolean | no | Require PKCE; can be disabled only for a confidential web application. Without PKCE, `nonce` is the only defense against authorization code injection, so `require_nonce` must stay on outside development environments (otherwise `validation_failed` on `/require_nonce`). |
| `skip_consent` | boolean | no | Skip the consent screen; first-party applications only. |
| `token_binding_policy` | string | no | `none` or `dpop_required`. |
| `login_host` | string or null | no | One of the environment's login hosts (lower-case DNS name). |
| `default_max_age_s` | integer or null | no | Default `max_age` in seconds. |
| `min_acr` | string or null | no | Minimum authentication assurance: `urn:idp:acr:1fa`, `urn:idp:acr:mfa` or `urn:idp:acr:phr`. |
| `access_token_lifetime_s` | integer or null | no | Access token lifetime, 60-86400 seconds. |
| `refresh_idle_s` | integer or null | no | Refresh token idle lifetime in seconds; not above `refresh_absolute_s`. |
| `refresh_absolute_s` | integer or null | no | Refresh token absolute lifetime in seconds. |
| `refresh_reuse_interval_s` | integer | no | Refresh token reuse window, 0-30 seconds (default 5). |
| `backchannel_logout_uri` | string or null | no | Back-channel logout URI (`https`). |
| `frontchannel_logout_uri` | string or null | no | Front-channel logout URI (`https`). |
| `frontchannel_logout_session_required` | boolean | no | Adds `iss` and `sid` to the front-channel logout URI. Setting it to `true` while the environment's front-channel logout setting is off returns 422. Default `false`. |
| `initiate_login_uri` | string | no | OIDC login initiation URI (OpenID Connect Core 1.0 §4): after an IdP-initiated SAML sign-in the browser is sent here with `iss`, and the application starts its own authorization code flow with `state` and PKCE. Absolute `https` URL with a lower-case DNS host, no fragment or user information, at most 2048 bytes; not allowed for `m2m` and `management` applications. Only an application with this URI can be an IdP-initiated target. |
| `id_token_org_claims` | boolean | no | When `true` (default), the ID token of an organization sign-in carries `org_id`, `org_slug` and `org_roles`; when `false`, the ID token carries no organization claims. Access tokens are not affected, and ID tokens never carry permissions. |
| `policy_uri` | string or null | no | Privacy policy URI (`https`), shown on the consent screen. |
| `tos_uri` | string or null | no | Terms of service URI (`https`). |
| `client_uri` | string or null | no | Application home page URI (`https`). |
| `jwks` | object or null | no | Inline JWK Set (`{"keys":[…]}`): 1-5 public keys, at most 16 KiB; private key members are rejected. Mutually exclusive with `jwks_uri`. |
| `jwks_uri` | string or null | no | Remote JWK Set URI (`https`); mutually exclusive with `jwks`. |
| `software_id` | string or null | no | DCR applications only. |
| `software_version` | string or null | no | DCR applications only. |
| `m2m_token_lifetime_s` | integer or null | no | Token lifetime for `m2m` applications, 60-86400 seconds. |
| `redirect_uris` | string[] | no | Allowed redirect URIs, at most 50; must be empty for `m2m` and `management`. |
| `post_logout_redirect_uris` | string[] | no | Allowed post-logout redirect URIs, at most 50. |
| `connection_ids` | string[] | no | Connections enabled on the application (`conn_…`), ordered; the order is the button order. At most 100. |
| `allowed_first_factors` | string[] | no | First factors allowed on the application: values from `password`, `passkey`, `email_otp`, `magic_link`, `sms_otp`, `social`, `enterprise`, no duplicates; returned in this order. An empty list (the default) means the application adds no restriction. The set is intersected with the environment's `allowed_first_factors`; if no method is left, `422 policy_conflict`. |
| `allowed_response_modes` | string[] | no | The `response_mode` values the application may use in authorization and pushed authorization requests: `query`, `form_post` (one or both, no duplicates); returned in this order. Default `["query"]`: an application that receives the authorization response by form post adds `form_post` explicitly. An authorization request whose `response_mode` is not in the list is rejected; a request without `response_mode` uses `query`. An empty list returns 422. JARM modes (`jwt`, `query.jwt`, `fragment.jwt`, `form_post.jwt`) are not supported yet. Applications registered through dynamic client registration or a client ID metadata document get `["query"]`. |

## Response

`201 Created` with the application and an `ETag` header.

```json
{
  "id": "app_6f8d0cjgbmf0gtn2rg6pscp3cg",
  "client_id": "app_6f8d0cjgbmf0gtn2rg6pscp3cg",
  "version": 1,
  "created_at": "2026-10-05T10:00:01Z",
  "updated_at": "2026-10-05T10:00:01Z",
  "key": "portal",
  "name": "Portal",
  "app_type": "web",
  "party": "first_party",
  "status": "active",
  "redirect_uris": ["https://portal.example.com/cb"],
  "...": "all other fields from the field reference"
}
```

## Errors

| HTTP | `code` | When |
|---|---|---|
| 401 | `unauthenticated` | The key is missing, invalid or revoked. |
| 403 | `insufficient_permissions` | The key lacks the required permission. |
| 409 | `conflict` | An application with this `key` already exists. |
| 422 | `validation_failed` | A field is invalid; `errors[].pointer` names it (for example `/redirect_uris/0`). |
| 422 | `policy_conflict` | `allowed_first_factors` has no method in common with the environment's first-factor set. |
| 400 | `unknown_field` | The body has a field that is not part of the schema. |
| 403 | `quota_exceeded` | The plan limit for applications is reached. |

## Example

```bash
curl -X POST https://api.eu.keenauth.com/v1/applications \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"portal","name":"Portal","app_type":"web","party":"first_party","redirect_uris":["https://portal.example.com/cb"]}'
```

## Response fields

`id` and `client_id` are the application id (`app_…`); `version` is also returned as `ETag`; `created_at`, `updated_at`, `last_token_issued_at` and `last_token_asn` are read-only. Every writable field above is returned. Client secrets are never part of this resource.
