Here’s an end-to-end outline for implementing one-time-code (OTP) authentication with a custom UI when using Ory Kratos (identities) together with Ory Hydra (OAuth2). The key is: OTP is always executed inside Kratos self-service flows; Hydra delegates authentication to Kratos and consumes the resulting session. Prerequisites - In Kratos, enable the code method for passwordless, or for MFA, depending on your use case: - Passwordless login/registration via email/SMS: enable selfservice.methods.code.passwordless_enabled=true. [[One-time code](
https://www.ory.sh/docs/kratos/passwordless/one-time-code)] - MFA via email/SMS: enable selfservice.methods.code.mfa_enabled=true and configure the identifier in the identity schema. [[SMS and Email 2FA](
https://www.ory.sh/docs/kratos/mfa/mfa-via-sms)] - Configure email/SMS delivery (SMTP and/or SMS webhook). [[Email templates](
https://www.ory.sh/docs/kratos/emails-sms/custom-email-templates); [Send emails](
https://www.ory.sh/docs/kratos/emails-sms/sending-emails-smtp); [Send SMS](
https://www.ory.sh/docs/kratos/emails-sms/sending-sms)] - For OAuth2 with Hydra: set up your OAuth2 client to use your custom UI for login and consent, with login handled by Kratos. (The knowledge sources don’t include detailed Hydra ↔️ Kratos wiring steps; proceed if you already have login/consent handlers.) Flow A: Passwordless login (single-page custom UI) 1) App starts OAuth2 authorization with Hydra - Your frontend hits your backend’s /oauth2/authorize (Hydra). Hydra redirects the user to your login UI because they’re not authenticated. (Detailed Hydra steps aren’t in the sources.) 2) Create a Kratos login flow (browser) - Your login UI calls Kratos: GET /self-service/login/browser to create the flow and receive ui nodes and the ui.action URL. [[Integration basics](
https://www.ory.sh/docs/kratos/bring-your-own-ui/custom-ui-basic-integration)] 3) Step 1: Send OTP - Render only the email/phone field from the login flow’s ui nodes for method=code. - Submit to flow’s ui.action with: - method=code - identifier (email or phone matching your identity schema) - Kratos sends the OTP via email/SMS. [[One-time code](
https://www.ory.sh/docs/kratos/passwordless/one-time-code)] 4) Step 2: Verify OTP - After submission, Kratos returns a next-step login flow state containing a code input node. - Your UI renders the code input and posts back to ui.action with: - method=code - code=<6-digit> - On success, Kratos establishes a session. [[Self-service flows](
https://www.ory.sh/docs/kratos/self-service)] 5) Return control to Hydra - With a valid Kratos session cookie, redirect back to your backend’s login handler so Hydra can accept the login and proceed to consent and token issuance. (Hydra specifics not in sources.) Flow B: Passwordless registration via OTP (same single input UX) - If you want a unified “enter email, receive OTP” that registers users who don’t yet exist: - On the first submission, your backend determines whether the identifier exists and triggers either a registration or login flow accordingly. This requires custom UI logic. [[Custom UI advice](
https://github.com/ory/kratos/discussions/4294)] - Registration via OTP: 1) Create registration flow: GET /self-service/registration/browser. 2) Submit with method=code and identifier to send the OTP. [[One-time code](
https://www.ory.sh/docs/kratos/passwordless/one-time-code)] 3) Submit code to ui.action to complete registration. You can add a post-registration session hook to sign the user in directly. [[Self-service flows](
https://www.ory.sh/docs/kratos/self-service)] Flow C: MFA via OTP (AAL2) - For users already logged in at AAL1, to step-up: 1) Start a login flow with aal=aal2: GET /self-service/login/browser?aal=aal2. [[SMS and Email 2FA](
https://www.ory.sh/docs/kratos/mfa/mfa-via-sms)] 2) Submit method=code to send OTP to the configured channel (email or SMS). 3) Submit method=code with the received code to upgrade the session to AAL2. Key implementation notes - Always use the flow’s returned ui.action URL and ui nodes to know which fields to render and submit; there’s no separate “send code” or “validate code” endpoint. You always POST to the flow with method=code and the required fields. [[Integration basics](
https://www.ory.sh/docs/kratos/bring-your-own-ui/custom-ui-basic-integration)] - For SMS, configure an SMS channel webhook; Kratos posts code + metadata to your webhook. [[SMS and Email 2FA](
https://www.ory.sh/docs/kratos/mfa/mfa-via-sms)] - For email, configure SMTP and optionally customize templates like login_code.valid or registration_code.valid. [[Email templates](
https://www.ory.sh/docs/kratos/emails-sms/custom-email-templates)] - One-time code passwordless and MFA via code cannot be used simultaneously for the same project; enabling MFA via code disables using code for passwordless login. Plan your strategy accordingly. [[SMS and Email 2FA](
https://www.ory.sh/docs/kratos/mfa/mfa-via-sms)] If you need exact request bodies, fetch the flow (getLoginFlow/getRegistrationFlow) and inspect the ui.nodes; submit exactly those fields back to ui.action with method=code. [[Integration basics](
https://www.ory.sh/docs/kratos/bring-your-own-ui/custom-ui-basic-integration)]