
# Create a sign-in transaction

`POST /fapi/v1/transactions` · called from the environment's sign-in host

Starts a sign-in transaction for an application. The hosted sign-in page, UI components and headless clients call this
endpoint on the environment's sign-in host (a `keenauth.app` subdomain or a verified custom domain). The response sets the
transaction cookie `__Host-idp_ltx` (Secure, HttpOnly, SameSite=Lax, valid for 15 minutes); later steps use that cookie.

## Request

Headers:

| Header | Required | Description |
| --- | --- | --- |
| `Idp-Client` | yes | SDK identifier, `<package>/<version> (<runtime>/<version>)`. |
| `Origin` | yes | Must be exactly `https://<sign-in host>`. |

Body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `client_id` | string | yes | The application's client ID. |
| `redirect_uri` | string (URI) | yes | One of the application's registered redirect URIs. |

## Response

`201 Created` with the transaction object:

```json
{
  "schema_version": 1,
  "state": "identifier_pending",
  "next_steps": [{ "step": "identifier", "fields": ["identifier"] }],
  "csrf_token": "<token>",
  "expires_at": "2026-10-05T10:15:00Z"
}
```

Send `csrf_token` back in the `Idp-Csrf-Token` header on every state-changing request of this transaction.

When the authorization request names an organization that requires SSO, the answer can already be
`federated_redirect` with `redirect_url` (one connection: navigate the top-level window there) or carry
`sso_connections` (several: send the choice with the `sso_start` step).

## DPoP-bound headless transactions

A first-party SPA or native app on its own origin starts a headless sign-in with a `DPoP` header (RFC 9449): a proof
signed with a key the client keeps (WebCrypto non-extractable key in a browser, the platform key store on a device),
with `htm` `POST` and `htu` `https://<sign-in host>/fapi/v1/transactions`. The transaction is then bound to that key
instead of a cookie: no cookie is set and the object has no `csrf_token`. Every later request of the transaction sends a
new proof from the same key.

Extra body fields, checked with the rules of the authorization endpoint:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `code_challenge` | string | yes | PKCE S256 challenge (RFC 7636). |
| `code_challenge_method` | string | yes | `S256`. |
| `scope` | string | no | Space-separated scopes, for example `openid profile email offline_access`. |
| `state` | string | no | Returned with the authorization code. |
| `nonce` | string | no | The ID token `nonce`. |
| `organization` | string | no | Organization ID or name. |
| `ui_locales` | string | no | Preferred languages. |
| `resource` | string (URI) | no | Resource indicator (RFC 8707). |

`Origin` may be the sign-in host or one of the application's allowed web origins; a native app sends none. Only
first-party applications can start one. A field the authorization endpoint refuses returns `422 validation_failed` with
the OAuth error code in `errors[].code`. When the server answers `401 use_dpop_nonce`, sign a new proof with the value
of the `DPoP-Nonce` response header and retry. Sending these fields without a `DPoP` header returns
`422 validation_failed` (`dpop_required`).

## Switching the organization

An application that holds a front API access token for the signed-in user (see [Front API access token](/docs/api/fapi/front-api-token))
switches the user to another organization without a screen. Send the token in `Authorization: DPoP <token>` with a proof
of the token's key (with `ath`), and in the body `flow` `organization_switch`, the DPoP-bound headless fields above
(`client_id` of the token's application, `redirect_uri`, `code_challenge`, `state`, `nonce`, `scope`) and
`organization`, the `org_…` ID of an organization from `GET /fapi/v1/me/organizations`.

The request is decided at once with the token's session, as a sign-in that reuses the session would be: the
organization's policy (MFA, idle limit, SSO, IP list), consent and pending legal documents. The answer is `201` with a
`completed` transaction that carries `authorization_response`; exchange its `code` at the token endpoint with your
`code_verifier` and a proof of the same key. The new tokens carry the organization in `org_id`. No transaction is left
open and no cookie is set.

```json
{
  "schema_version": 1,
  "state": "completed",
  "next_steps": [],
  "authorization_response": { "code": "<code>", "state": "<state>", "iss": "https://example.keenauth.app" },
  "expires_at": "2026-10-05T10:15:00Z"
}
```

| Status | Code | When |
| --- | --- | --- |
| 401 | `unauthenticated` | The token is invalid, expired, revoked or not bound to a key, its application is not an active first-party application, or its session has ended. |
| 422 | `validation_failed` | `flow` is not `organization_switch`, `client_id` is not the token's application, `organization` is not an `org_…` ID, or the user cannot switch to it in this application (`not_eligible`). |
| 403 | `authorization_failed` | The organization needs a screen the switch cannot show; `error` is the OAuth error (`login_required`, `interaction_required`, `consent_required`). Start a new sign-in with `organization` set. |

`flow` `organization_switch` without the token returns `422 validation_failed` (`access_token_required`).

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `malformed_request` | `Idp-Client` is missing or the body is not valid JSON. |
| 403 | `origin_not_allowed` | `Origin` is not the sign-in host. |
| 403 | `headless_not_allowed` | A third-party application tried to start a headless flow. |
| 404 | `not_found` | The host is not a sign-in host of any environment. |
| 422 | `validation_failed` | `client_id` is not an application of this environment, `redirect_uri` is not registered for it, or a field is invalid. |
| 401 | `invalid_dpop_proof` | The `DPoP` proof is missing, invalid or reused, or the host does not serve DPoP-bound transactions. |
| 401 | `use_dpop_nonce` | Sign a new proof with the `DPoP-Nonce` response header value. |
| 503 | `temporarily_unavailable` | A dependency is unavailable; retry later. |

## Example

```bash
curl -X POST https://example.keenauth.app/fapi/v1/transactions \
  -H "Idp-Client: my-app/1.0.0 (node/22.0.0)" \
  -H "Origin: https://example.keenauth.app" \
  -H "Content-Type: application/json" \
  -c cookies.txt \
  -d '{"client_id":"<client_id>","redirect_uri":"https://app.example.com/callback"}'
```

## Personal data

This endpoint carries personal data: end-user profile. [Personal data in the KeenAuth APIs](/docs/policies/personal-data) describes what is stored, how long it is kept and how it is deleted. Do not send special categories of personal data (health, biometric data, religion, political opinions, sexual orientation) in any field.
