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:
{
"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)
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.
{
"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
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 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.