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.