careful-honey-27437
06/13/2025, 8:47 PMbrief-egg-61061
06/13/2025, 8:47 PMbrief-egg-61061
06/13/2025, 8:47 PM@ory/client
) to initiate the flow. This example demonstrates how to create a native login flow and submit the identifier to start the OTC process. typescript import { Configuration, FrontendApi } from "@ory/client" const frontend = new FrontendApi( new Configuration({ basePath: `https://${process.env.ORY_PROJECT_SLUG}.<http://projects.oryapis.com|projects.oryapis.com>`, }), ) async function initiateOtcLogin(email: string) { // 1. Create a native login flow const { data: flow } = await frontend.createNativeLoginFlow() // 2. Submit the identifier to trigger the OTC (one-time code) method await frontend.updateLoginFlow({ flow: flow.id, updateLoginFlowBody: { method: "code", // "code" is the method for OTC identifier: email, }, }) // At this point, Ory will send a one-time code to the provided email. } initiateOtcLogin("<mailto:user@example.com|user@example.com>").catch(console.error)
- Replace process.env.ORY_PROJECT_SLUG
with your actual Ory project slug or set it in your environment. - The method: "code"
field is used to indicate the OTC passwordless method. - The user will receive a code via email, which they must enter in the next step to complete authentication. This flow is based on the documented process for enabling and using one-time code passwordless login in Ory Network, and the usage of the Ory SDK for Node.js/TypeScript to interact with the HTTP API [Passwordless login](https://www.ory.sh/docs/identities/get-started/passwordless) [Create login flow](https://www.ory.sh/docs/kratos/bring-your-own-ui/custom-ui-basic-integration#create-login-flow).careful-honey-27437
06/13/2025, 8:47 PMbrief-egg-61061
06/13/2025, 8:47 PMbrief-egg-61061
06/13/2025, 8:48 PM/self-service/login/api
. 2. Submit the identifier (e.g., email) and specify the code
method to trigger the OTC process. Below is a TypeScript example using axios: typescript import axios from "axios" const ORY_PROJECT_SLUG = process.env.ORY_PROJECT_SLUG // Set your Ory project slug const BASE_URL = `https://${ORY_PROJECT_SLUG}.<http://projects.oryapis.com|projects.oryapis.com>` async function initiateOtcLogin(email: string) { // 1. Create a native login flow const flowRes = await axios.get(`${BASE_URL}/self-service/login/api`) const flow = flowRes.data // 2. Submit the identifier to trigger the OTC (one-time code) method await <http://axios.post|axios.post>( flow.ui.action, { method: "code", // Use the "code" method for OTC identifier: email, // The user's email address }, { headers: { "Content-Type": "application/json" }, } ) // At this point, Ory will send a one-time code to the provided email. } initiateOtcLogin("<mailto:user@example.com|user@example.com>").catch(console.error)
- The first request creates a login flow and retrieves the ui.action
URL for the next step. - The second request posts the identifier and method to the ui.action
endpoint, which triggers the sending of the one-time code to the user’s email. This approach is directly based on the Ory documentation for creating and submitting login flows using the HTTP API, and the structure of the flow object as shown in the docs [Create a flow](https://www.ory.sh/docs/kratos/bring-your-own-ui/custom-ui-basic-integration#create-a-flow) and [Create login flow](https://www.ory.sh/docs/kratos/bring-your-own-ui/custom-ui-basic-integration#create-login-flow).careful-honey-27437
06/13/2025, 8:48 PMbrief-egg-61061
06/13/2025, 8:49 PMbrief-egg-61061
06/13/2025, 8:49 PMui.action
URL from the login flow, including the code and the method. Here’s an example using axios in TypeScript/Node.js: typescript import axios from "axios" const ORY_PROJECT_SLUG = process.env.ORY_PROJECT_SLUG // Set your Ory project slug const BASE_URL = `https://${ORY_PROJECT_SLUG}.<http://projects.oryapis.com|projects.oryapis.com>` async function completeOtcLogin(flowId: string, code: string) { // 1. Fetch the login flow to get the ui.action URL const flowRes = await axios.get(`${BASE_URL}/self-service/login/flows?id=${flowId}`) const flow = flowRes.data // 2. Submit the code to attain a session const response = await <http://axios.post|axios.post>( flow.ui.action, { method: "code", // Use the "code" method for OTC code: code, // The one-time code received by the user }, { headers: { "Content-Type": "application/json" }, // You may need to include cookies if required by your flow } ) // If successful, the response will include the session information or set a session cookie return response.data } // Usage example completeOtcLogin("your-login-flow-id", "123456").then(console.log).catch(console.error)
How it works: - The user receives a code via email or SMS after initiating the OTC login. - You fetch the login flow (using its ID) to get the correct ui.action
endpoint. - You POST the code and method to the ui.action
endpoint. - If the code is valid, Ory will create a session for the user. This flow is described in the Ory documentation for one-time code authentication and custom UI integrations. The exact field names and endpoints are confirmed in the docs, but the knowledge sources do not provide a full HTTP request/response example for this step. The above example is constructed based on the documented flow and field names for submitting the code to the login flow endpoint [Email and SMS one-time-code authentication](https://www.ory.sh/docs/kratos/passwordless/one-time-code) and [Custom UI basic integration](https://www.ory.sh/docs/kratos/bring-your-own-ui/custom-ui-basic-integration#session-checks). If you need to check the session after login, you can use the /sessions/whoami
endpoint as described in the docs [Ory API Reference](https://www.ory.sh/docs/reference/api).careful-honey-27437
06/13/2025, 11:01 PM${ORY_BASE_URL}/self-service/login/api
)
const flow = flowRes.data
console.log(flow)
// 2. Submit the identifier to trigger the OTC (one-time code) method
const otc_login_result = await axios.post(
flow.ui.action,
{
method: “code”, // Use the “code” method for OTC
identifier: email, // The user’s email address
},
{
headers: {
“Content-Type”: “application/json”
},
}
)
console.log(JSON.stringify(otc_login_result, null, 3))
return otc_login_result
}brief-egg-61061
06/13/2025, 11:01 PMbrief-egg-61061
06/13/2025, 11:01 PM400 Bad Request
error when submitting the identifier to trigger the OTC (one-time code) login. Based on the Ory documentation and community discussions, here are the most likely reasons: ### 1. Incorrect Quotes Your code uses “smart quotes” (“
and ”
) instead of standard ASCII quotes ("
). This will cause a syntax error in JavaScript/TypeScript and may result in an invalid JSON payload being sent to the Ory API, which would trigger a 400 error. Fix: typescript { method: "code", // Use the "code" method for OTC identifier: email, // The user’s email address }
### 2. Missing Required Fields For the code method, the Ory API expects at least the method
and csrf_token
fields in some flows (especially browser flows). For native flows, CSRF may not be required, but if the flow expects it and it's missing, you will get a 400 error. Double-check the flow type and required fields in the flow.ui.nodes
array. ### 3. Incorrect Endpoint or Flow State If the flow is expired, already used, or the endpoint is not correct (e.g., using a browser flow endpoint for an API flow), you will get a 400 error. Always use the ui.action
URL from the flow object you just fetched. ### 4. Payload Validation A 400 error is returned if the payload is malformed or missing required fields. The Ory API expects the payload to match the expected schema for the login method. For the code method, the required fields are typically method
and identifier
(and possibly csrf_token
for browser flows) [see API reference](https://www.ory.sh/docs/reference/api). ### 5. Flow Already Completed or Invalid If you try to reuse a flow that has already been completed or is expired, Ory will return a 400 error. Always use a fresh flow for each login attempt. --- #### Example of a Correct Request (with standard quotes): typescript const otc_login_result = await <http://axios.post|axios.post>( flow.ui.action, { method: "code", identifier: email, // csrf_token: "..." // Only if required, check flow.ui.nodes for this field }, { headers: { "Content-Type": "application/json" }, } )
--- #### Troubleshooting Steps - Check the response body of the 400 error. Ory provides detailed error messages in the response body, which can help pinpoint the exact issue. - Inspect the flow object (console.log(flow)
) to see if there are any required fields (like csrf_token
) in the ui.nodes
. - Ensure you are using the correct endpoint (flow.ui.action
) and that the flow is fresh. --- #### Reference This behavior is consistent with Ory's documented API and community feedback, where 400 errors are returned for invalid or incomplete payloads, missing required fields, or expired/invalid flows. Developers have reported similar confusion when the API returns 400 for what appears to be a valid request, but the underlying issue is usually a missing or malformed field, or an expired flow [see discussion](https://github.com/ory/kratos/issues/4052) and [form submission and payload validation](https://www.ory.sh/docs/kratos/self-service#form-submission-and-payload-validation-2). If you continue to see 400 errors, check the error message in the response body for more details.careful-honey-27437
06/13/2025, 11:11 PMcsrf
token need to be, and where do I obtain it?brief-egg-61061
06/13/2025, 11:11 PMbrief-egg-61061
06/13/2025, 11:12 PMcsrf_token
must match the token provided by Ory in the flow object when you initiate a self-service flow (such as login, registration, recovery, or verification). You obtain the csrf_token
from the ui.nodes
array in the flow response, where it appears as a hidden input field named csrf_token
. For example, after you create a login flow, the response will include something like: json { "ui": { "nodes": [ { "type": "input", "group": "default", "attributes": { "name": "csrf_token", "type": "hidden", "value": "XmG3qwTYSV0oWIyNGTugvtNOKMxWPYHd7dNX7BYK5lL79P0iUdq5jVmRUKwwm8RLcAGN7eF7iYraAiTSOdamuQ==", "required": true, "disabled": false }, "messages": [], "meta": {} } ] } }
You must extract the value of the csrf_token
from this node and include it in your subsequent POST request to submit the flow. The token is unique per flow and must not be made up or reused from another flow. This is required for browser flows to protect against cross-site request forgery attacks. The browser will also send a CSRF cookie automatically, but you must manually include the token value in your request body as shown above [see documentation](https://www.ory.sh/docs/kratos/bring-your-own-ui/custom-ui-basic-integration#debug) [see form rendering](https://www.ory.sh/docs/kratos/self-service#form-rendering). In summary: - The csrf_token
value is provided by Ory in the flow response. - You must extract it from the flow's ui.nodes
and include it in your form or API request when submitting the flow.careful-honey-27437
06/13/2025, 11:12 PMcareful-honey-27437
06/13/2025, 11:13 PMbrief-egg-61061
06/13/2025, 11:13 PMbrief-egg-61061
06/13/2025, 11:13 PMcsrf_token
node in the response has an empty value (i.e., "value": ""
), this is expected behavior for API (native) flows in Ory Kratos. According to the Ory documentation and quickstart examples, the CSRF token is only required and populated for browser flows. For API/native flows, the CSRF token is not used, and the value will be empty. You do not need to include a CSRF token when submitting the flow in this case [see quickstart example](https://www.ory.sh/docs/kratos/quickstart#perform-registration-login-and-logout). Here’s an example from the documentation: json { "type": "input", "group": "default", "attributes": { "name": "csrf_token", "type": "hidden", "value": "", "required": true, "disabled": false, "node_type": "input" }, "messages": [], "meta": {} }
So, if you are using the native (API) flow for OTC login, you can safely omit the csrf_token
from your submission payload, or send it as an empty string. This is consistent with the documented behavior and examples provided by Ory [see quickstart](https://www.ory.sh/docs/kratos/quickstart#perform-registration-login-and-logout). If you are using a browser flow and the value is still empty, that would be unexpected and could indicate a misconfiguration, but for API flows, this is correct and you do not need to worry about the CSRF token.careful-honey-27437
06/13/2025, 11:13 PMbrief-egg-61061
06/13/2025, 11:13 PMbrief-egg-61061
06/13/2025, 11:14 PMGET /self-service/login/api
You should send a GET request to this endpoint on your Ory project base URL. This endpoint is specifically for native applications (such as mobile apps or other non-browser clients) and is the correct way to start a login flow that can use the OTC method. For example: https://<your-project-slug>.<http://projects.oryapis.com/self-service/login/api|projects.oryapis.com/self-service/login/api>
This is confirmed in the Ory documentation for native login flows and is the correct starting point for an OTC login in a native app context [see Ory API Reference](https://www.ory.sh/docs/reference/api) and [custom UI integration basics](https://www.ory.sh/docs/kratos/bring-your-own-ui/custom-ui-basic-integration#create-login-flow).careful-honey-27437
06/13/2025, 11:14 PMbrief-egg-61061
06/13/2025, 11:15 PMbrief-egg-61061
06/13/2025, 11:15 PMGET /self-service/login/browser
You should send a GET request to this endpoint on your Ory project base URL. For example: https://<your-project-slug>.<http://projects.oryapis.com/self-service/login/browser|projects.oryapis.com/self-service/login/browser>
This endpoint is specifically for browser-based applications and will set the appropriate cookies and anti-CSRF measures required for browser flows. This is confirmed in the Ory documentation for creating a browser login flow [Create login flow](https://www.ory.sh/docs/kratos/bring-your-own-ui/custom-ui-basic-integration#create-login-flow) and [API Reference](https://www.ory.sh/docs/kratos/reference/api).careful-honey-27437
06/14/2025, 3:26 AM<https://bold-kepler-liove18x32.projects.oryapis.com/self-service/login?flow=3b0f2eab-66de-4618-96d5-6c3ffcb6a5d6>
{
"message": "Request failed with status code 400",
"name": "AxiosError",
"stack": "AxiosError: Request failed with status code 400\n at settle (webpack-internal:///(rsc)/./node_modules/axios/lib/core/settle.js:24:12)\n at IncomingMessage.handleStreamEnd (webpack-internal:///(rsc)/./node_modules/axios/lib/adapters/http.js:629:71)\n at IncomingMessage.emit (node:events:531:35)\n at endReadableNT (node:internal/streams/readable:1696:12)\n at process.processTicksAndRejections (node:internal/process/task_queues:82:21)\n at Axios.request (webpack-internal:///(rsc)/./node_modules/axios/lib/core/Axios.js:57:41)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async initiateOTCLogin (webpack-internal:///(rsc)/./src/collections/subscriptions/subscribe/subscribe.ts:98:30)\n at async subscribe (webpack-internal:///(rsc)/./src/collections/subscriptions/subscribe/subscribe.ts:183:30)\n at async handleEndpoints (file:///Users/ajb/Server/IMG/img-api/node_modules/payload/dist/utilities/handleEndpoints.js:168:26)\n at async eval (webpack-internal:///(rsc)/./node_modules/@payloadcms/next/dist/routes/rest/index.js:30:20)\n at async <http://AppRouteRouteModule.do|AppRouteRouteModule.do> (/Users/ajb/Server/IMG/img-api/node_modules/next/dist/compiled/next-server/app-route.runtime.dev.js:26:33891)\n at async AppRouteRouteModule.handle (/Users/ajb/Server/IMG/img-api/node_modules/next/dist/compiled/next-server/app-route.runtime.dev.js:26:41254)\n at async doRender (/Users/ajb/Server/IMG/img-api/node_modules/next/dist/server/base-server.js:1513:42)\n at async DevServer.renderToResponseWithComponentsImpl (/Users/ajb/Server/IMG/img-api/node_modules/next/dist/server/base-server.js:1915:28)\n at async DevServer.renderPageComponent (/Users/ajb/Server/IMG/img-api/node_modules/next/dist/server/base-server.js:2393:24)\n at async DevServer.renderToResponseImpl (/Users/ajb/Server/IMG/img-api/node_modules/next/dist/server/base-server.js:2430:32)\n at async DevServer.pipeImpl (/Users/ajb/Server/IMG/img-api/node_modules/next/dist/server/base-server.js:1003:25)\n at async NextNodeServer.handleCatchallRenderRequest (/Users/ajb/Server/IMG/img-api/node_modules/next/dist/server/next-server.js:304:17)\n at async DevServer.handleRequestImpl (/Users/ajb/Server/IMG/img-api/node_modules/next/dist/server/base-server.js:895:17)\n at async /Users/ajb/Server/IMG/img-api/node_modules/next/dist/server/dev/next-dev-server.js:371:20\n at async Span.traceAsyncFn (/Users/ajb/Server/IMG/img-api/node_modules/next/dist/trace/trace.js:157:20)\n at async DevServer.handleRequest (/Users/ajb/Server/IMG/img-api/node_modules/next/dist/server/dev/next-dev-server.js:368:24)\n at async invokeRender (/Users/ajb/Server/IMG/img-api/node_modules/next/dist/server/lib/router-server.js:247:21)\n at async handleRequest (/Users/ajb/Server/IMG/img-api/node_modules/next/dist/server/lib/router-server.js:438:24)\n at async requestHandlerImpl (/Users/ajb/Server/IMG/img-api/node_modules/next/dist/server/lib/router-server.js:462:13)\n at async Server.requestListener (/Users/ajb/Server/IMG/img-api/node_modules/next/dist/server/lib/start-server.js:158:13)",
"config": {
"transitional": {
"silentJSONParsing": true,
"forcedJSONParsing": true,
"clarifyTimeoutError": false
},
"adapter": [
"xhr",
"http",
"fetch"
],
"transformRequest": [
null
],
"transformResponse": [
null
],
"timeout": 0,
"xsrfCookieName": "XSRF-TOKEN",
"xsrfHeaderName": "X-XSRF-TOKEN",
"maxContentLength": -1,
"maxBodyLength": -1,
"env": {},
"headers": {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json",
"Authentication": "Bearer <valid ory api token here>",
"User-Agent": "axios/1.8.4",
"Content-Length": "155",
"Accept-Encoding": "gzip, compress, deflate, br"
},
"method": "post",
"url": "<https://bold-kepler-liove18x32.projects.oryapis.com/self-service/login?flow=3b0f2eab-66de-4618-96d5-6c3ffcb6a5d6>",
"data": "{\"method\":\"code\",\"identifier\":\"<mailto:ajb+test1@visia.ca|ajb+test1@visia.ca>\",\"csrf_token\":\"Zn1tqBDdYt+8RtqNVVSADqaSJbUhD9Y8THYPk91i6gN4tE7/cVzKLrrvdqng5HmTA6z7aJtTzQQwrzSV35xEQw==\"}",
"allowAbsoluteUrls": true
},
"code": "ERR_BAD_REQUEST",
"status": 400
}
brief-egg-61061
06/14/2025, 3:26 AMbrief-egg-61061
06/14/2025, 3:26 AM400 Bad Request
when calling `https://bold-kepler-liove18x32.projects.oryapis.com/self-service/login?flow=3b0f2eab-66de-4618-96d5-6c3ffcb6a5d6`: --- ### 1. Incorrect Use of API vs. Browser Flow You are using the /self-service/login?flow=...
endpoint, which is intended for browser flows. For API/native flows, you should use the /self-service/login/api
endpoint to initiate the flow and then submit to the ui.action
URL provided in the flow object. Submitting to the wrong endpoint or mixing up browser and API flows can result in a 400 error [see API reference](https://www.ory.sh/docs/kratos/reference/api). --- ### 2. CSRF Token Issues For browser flows, the CSRF token is required and must be valid. If the CSRF token is missing, empty, or does not match what Ory expects, you will get a 400 error. For API/native flows, the CSRF token is not required and is typically empty. If you are using a browser flow, ensure you are sending the correct CSRF token as provided in the flow's ui.nodes
. If the value is empty, that may indicate you are using an API flow, in which case you should not include the CSRF token [see CSRF troubleshooting](https://www.ory.sh/docs/troubleshooting/csrf). --- ### 3. Flow Expiry or Invalid Flow If the flow has expired or is no longer valid (for example, if it has already been used or is too old), Ory will return a 400 error. Always use a fresh flow for each login attempt. The error response body from Ory will usually include an error.id
such as self_service_flow_expired
if this is the case [see API reference](https://www.ory.sh/docs/kratos/reference/api). --- ### 4. Malformed or Unexpected Payload If the payload does not match what Ory expects for the flow and method, you will get a 400 error. Double-check that you are sending the correct fields (method
, identifier
, and csrf_token
if required) and that their values are correct. Also, ensure you are not sending extra or malformed fields. --- ### 5. Authentication Header The Authentication
header is not standard for Ory public self-service endpoints. Unless you are calling an admin or private endpoint, you should not need to send an Authentication
header. For self-service flows, authentication is handled via cookies and CSRF tokens, not bearer tokens. --- ### What to Do Next - Check the error response body from Ory for more details. It often contains an error.id
and a message explaining the problem. - Ensure you are using the correct flow type and endpoint (browser vs. API). - Use the ui.action
URL from the flow object for your POST request, not the generic /self-service/login?flow=...
unless you are following the browser flow pattern. - Double-check the CSRF token: For browser flows, it must be present and valid; for API flows, it should be omitted or empty. - Do not send an Authentication
header unless specifically required by the endpoint. --- If you need more specific help, please provide the full error response body from Ory (not just the Axios error), as it will contain the exact reason for the 400 error. --- This answer is based on the Ory documentation and real-world issues reported by other developers using Ory Kratos [see API reference](https://www.ory.sh/docs/kratos/reference/api) [see CSRF troubleshooting](https://www.ory.sh/docs/troubleshooting/csrf) [see GitHub issue discussion](https://github.com/ory/kratos/issues/4052).careful-honey-27437
06/14/2025, 6:48 AM/self-service/login/api
to initiate an OTC passwordless login, and it’s creating the OTC, and sending the email, but the response I’m getting from the API request is a 400 error, why?brief-egg-61061
06/14/2025, 6:48 AMcareful-honey-27437
06/14/2025, 6:48 AMbrief-egg-61061
06/14/2025, 6:48 AM/self-service/login/api
, and the code is created and the email is sent, but you receive a 400 error in the API response, this is a known and documented behavior in Ory Kratos. Why does this happen? - Ory currently returns a 400 Bad Request
status code for incomplete flows, even if the request is valid and the next step (such as sending the OTC email) was successfully triggered. This means that even though the email is sent, the API responds with 400, which can be confusing because it suggests a client error when, in fact, the process is working as intended. - This behavior is acknowledged by the Ory team and discussed in detail in [this GitHub issue](https://github.com/ory/kratos/issues/4052). The rationale is that Ory only returns 200 OK
when the login flow is fully completed (i.e., the user is authenticated and a session is created). For intermediate steps—such as sending an OTC email—Ory returns a non-success status code (like 400) to indicate the flow is not yet finished. Key points from the discussion: - The 400 status does not necessarily mean your request was malformed; it can also mean the flow is not yet complete. - The response body will contain information about the next step, such as a message indicating that an email with a code has been sent. - This behavior is planned to be improved in a future major version, where more granular and appropriate status codes (like 202 for "accepted, processing") may be used, but for now, 400 is expected for these intermediate steps. What should you do? - Parse the response body for messages or instructions, rather than relying solely on the HTTP status code to determine if the step was successful. - If you see a message in the response like "An email containing a code has been sent to the email address you provided," you can proceed to the next step in the flow, even if the status code is 400. References from the knowledge sources: - "We will still return a non-success code for any form fill that is incomplete or being processed. 200 OK will only ever be sent when the flow is successfully completed." - "400 response, in this case, sends a wrong signal that the server could not process the request even though it successfully did. ... The issue is that we talk to the API and the API gives us misleading status codes, so we can't rely on the status codes to detect whether our request was successful or not, which is the very point of status codes, like you normally do for any HTTP API that properly implements status codes." - "This behavior is expected. 200 is only sent on successful login." [Requesting a login code returns HTTP 400](https://github.com/ory/kratos/issues/4052) In summary: A 400 error after initiating an OTC login is expected in the current Ory Kratos implementation. Check the response body for confirmation that the code was sent, and proceed with the next step of the login flow.