LangFlow API Deployment: Turning a Flow Into a Callable Endpoint
The Moment Your Flow Stops Being a Toy
You've spent an afternoon in the LangFlow canvas. You dragged in a prompt node, wired up an OpenAI component, added a vector store for retrieval, and clicked "Run" a dozen times until the output looked right. It works. You're happy. Then someone on your team asks the obvious question: "Cool, so how does the mobile app call this?"
That question is where most LangFlow tutorials stop and where the real engineering work begins. A flow sitting in the LangFlow UI is a prototype. A flow exposed as an HTTP endpoint that your frontend, your backend service, or a cron job can hit with a POST request is a product feature. The gap between those two states is smaller than people expect, but it's full of details that trip people up the first time: which endpoint to call, how to pass input variables, how session memory actually persists between calls, how to secure the thing before you put it in front of real users, and how to keep the API contract stable when you're still tweaking the flow in the visual editor.
This article walks through that entire gap. We'll take a simple flow, expose it via the LangFlow REST API, call it from curl, Python, and JavaScript, handle streaming responses, manage sessions, add authentication, and talk about what changes when you move from your laptop to a real deployment. By the end you should be able to look at any flow you've built and know exactly what it takes to turn it into something an application can depend on.
How LangFlow Actually Exposes Flows as APIs
Every flow you build in LangFlow already has an API behind it — you just haven't been using it directly. When you open a flow in the editor, LangFlow assigns it a flow ID (a UUID) and automatically generates a corresponding REST endpoint. You don't need to write any server code, define routes, or set up a separate API gateway. The moment the flow is saved, it's callable.
The core endpoint you'll use most is:
POST /api/v1/run/{flow_id}This single endpoint runs your entire flow end to end — every node, every chain, every tool call — and returns the final output as JSON. The {flow_id} is visible in your browser's address bar when you have the flow open, and it's also listed on the flow's card in the LangFlow dashboard, or accessible via the "API access" panel that LangFlow generates for every flow.
There's also a second, closely related endpoint:
POST /api/v1/run/{flow_id}?stream=trueSame flow, same inputs, but the response comes back as a stream of server-sent events instead of one blocked JSON payload. We'll get to that later — first let's nail the basic request/response cycle.
A minimal request body looks like this:
{
"input_value": "Summarize the key risks in this contract clause.",
"output_type": "chat",
"input_type": "chat"
}input_valueis whatever you'd normally type into the chat box in the LangFlow playground.input_typeandoutput_typetell LangFlow whether to treat this as a chat message or a raw text value, which determines how it's routed into your flow's input/output components.- If your flow has a
Chat Inputcomponent and aChat Outputcomponent,chat/chatis what you want in almost every case.
That's the whole contract. No custom schema to define, no OpenAPI spec to hand-write. LangFlow inspects your flow graph and figures out where input_value should land.
Your First Real curl Call
Let's make this concrete. Assume you're running LangFlow locally on the default port, and you have a flow ID of 4f6c2b1e-9a3d-4e21-8b7f-1c9d2e4a5f60. Here's the full curl call:
curl -X POST \
"http://localhost:7860/api/v1/run/4f6c2b1e-9a3d-4e21-8b7f-1c9d2e4a5f60" \
-H "Content-Type: application/json" \
-d '{
"input_value": "What are three risks in a month-to-month lease with no notice clause?",
"output_type": "chat",
"input_type": "chat"
}'If everything is wired correctly, you'll get back a JSON blob that includes the full run trace — every component's inputs and outputs — plus a top-level outputs array where the final message lives. The part you actually care about, most of the time, is buried a few levels deep:
{
"outputs": [
{
"inputs": { "input_value": "What are three risks..." },
"outputs": [
{
"results": {
"message": {
"text": "1. No fixed term means either party can terminate with minimal notice...\n2. Rent increases can happen with little warning...\n3. No renewal guarantee, which complicates long-term planning..."
}
}
}
]
}
]
}That nested structure is one of the first things people find awkward about the LangFlow API — the payload mirrors the internal graph execution, not a clean "here's your answer" shape. In practice, you'll write a small extraction helper once and reuse it everywhere:
def extract_message(response_json: dict) -> str:
"""Pull the final chat message text out of a LangFlow run response."""
try:
outputs = response_json["outputs"][0]["outputs"][0]
return outputs["results"]["message"]["text"]
except (KeyError, IndexError, TypeError):
raise ValueError(f"Unexpected response shape: {response_json}")Wrap this once and every downstream caller in your codebase gets a clean string instead of hand-rolling the same dictionary traversal five times.
Calling It From Python
Most teams end up wrapping the raw API in a thin client, especially if the flow is going to be called from a backend service rather than curl. Here's a reasonably complete example using the requests library:
import requests
LANGFLOW_URL = "http://localhost:7860"
FLOW_ID = "4f6c2b1e-9a3d-4e21-8b7f-1c9d2e4a5f60"
API_KEY = "sk-lf-your-api-key-here"
def run_flow(message: str, session_id: str | None = None) -> str:
url = f"{LANGFLOW_URL}/api/v1/run/{FLOW_ID}"
payload = {
"input_value": message,
"output_type": "chat",
"input_type": "chat",
}
if session_id:
payload["session_id"] = session_id
headers = {
"Content-Type": "application/json",
"x-api-key": API_KEY,
}
response = requests.post(url, json=payload, headers=headers, timeout=60)
response.raise_for_status()
data = response.json()
return data["outputs"][0]["outputs"][0]["results"]["message"]["text"]
if __name__ == "__main__":
answer = run_flow("Explain the difference between a promissory note and a lease agreement.")
print(answer)A few things worth calling out here because they matter in production:
- Always set a timeout. Flows that call an LLM plus a retriever plus a reranker can take 10-30 seconds under load. The default
requestsbehavior with no timeout will hang your calling service forever if LangFlow stalls. - `response.raise_for_status()` turns HTTP errors into Python exceptions instead of silently returning a 500-page HTML blob that breaks your JSON parsing three lines later.
- `session_id` is optional but important — more on that in the next section.
Calling It From JavaScript / Node
The same call from a Node backend or a frontend fetch call looks like this:
async function runFlow(message, sessionId = null) {
const url = `http://localhost:7860/api/v1/run/${process.env.LANGFLOW_FLOW_ID}`;
const payload = {
input_value: message,
output_type: "chat",
input_type: "chat",
};
if (sessionId) payload.session_id = sessionId;
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.LANGFLOW_API_KEY,
},
body: JSON.stringify(payload),
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`LangFlow API error (${res.status}): ${errorText}`);
}
const data = await res.json();
return data.outputs[0].outputs[0].results.message.text;
}
// usage
runFlow("Draft a polite follow-up email for an unpaid invoice.")
.then((text) => console.log(text))
.catch((err) => console.error(err));If you're calling this from a browser directly (rather than through your own backend), think twice about it. Your API key would be exposed in client-side network traffic, and CORS configuration on your LangFlow instance would need to allow the calling origin. The safer pattern in nearly every real app is: frontend calls your backend, your backend calls LangFlow, and the API key never leaves your server.
Handling Sessions and Conversation Memory
If your flow includes a memory component — anything that remembers prior turns in a conversation — you need to pass a session_id on every request. Without it, LangFlow treats each call as a brand-new conversation with no history, which is almost never what you want for a chat-style feature.
{
"input_value": "And what about the second option I mentioned?",
"output_type": "chat",
"input_type": "chat",
"session_id": "user-8842-conversation-1"
}The session_id is just a string you generate and manage on your side — LangFlow doesn't issue it for you. A common pattern is to derive it from your own user and conversation identifiers:
import uuid
def make_session_id(user_id: str, conversation_id: str) -> str:
return f"{user_id}:{conversation_id}"
# or, if you don't have a stable conversation id yet:
new_session_id = str(uuid.uuid4())Keep the session ID consistent across every request in a conversation, and generate a new one whenever the user starts a fresh conversation. If you skip this and reuse a single hardcoded session ID across all users, you'll get the bizarre bug where User A's questions start showing up in User B's answers because they're sharing the same memory buffer inside LangFlow.
One more detail: session memory in LangFlow is typically backed by whatever storage the memory component is configured with — in-memory for local dev, or a database-backed store for anything persistent. If you restart your LangFlow instance and it was using in-memory storage, all active sessions vanish. For production deployments, make sure your memory component points at persistent storage, not the default in-memory option.
Overriding Component Values Per Request (Tweaks)
Sometimes you don't want to change the flow itself for every variation — you want to keep one flow and adjust a parameter per call. LangFlow supports this through the tweaks field, which lets you override any component's input value at request time without touching the flow definition.
curl -X POST \
"http://localhost:7860/api/v1/run/4f6c2b1e-9a3d-4e21-8b7f-1c9d2e4a5f60" \
-H "Content-Type: application/json" \
-H "x-api-key: sk-lf-your-api-key-here" \
-d '{
"input_value": "Summarize this in a friendly tone.",
"output_type": "chat",
"input_type": "chat",
"tweaks": {
"OpenAIModel-abc123": {
"temperature": 0.9,
"model_name": "gpt-4o-mini"
}
}
}'The keys inside tweaks are component IDs — you can find these in the flow's exported JSON, or via the "API access" code snippet panel LangFlow generates in the UI, which already shows you the correct component ID strings for the flow you have open. This is genuinely one of the more useful features for teams running the same flow across multiple use cases: one flow, same graph, different temperature or system prompt per calling context, all controlled from the API request instead of maintaining five near-duplicate flows.
A practical use case: you have one "customer support responder" flow, but you want a stricter, lower-temperature response for billing questions and a warmer, higher-temperature response for general chat. Rather than duplicating the flow, you pass different tweaks depending on which category your router already classified the message into.
Streaming Responses for a Better User Experience
Waiting 15 seconds for a single JSON blob to arrive feels broken to end users, even when the backend is working exactly as intended. LangFlow supports streaming via server-sent events, which lets you show tokens as they're generated — the same experience you get from ChatGPT's UI.
Enable it by adding stream=true to the run endpoint:
curl -N -X POST \
"http://localhost:7860/api/v1/run/4f6c2b1e-9a3d-4e21-8b7f-1c9d2e4a5f60?stream=true" \
-H "Content-Type: application/json" \
-d '{
"input_value": "Write a short product description for a wireless keyboard.",
"output_type": "chat",
"input_type": "chat"
}'The -N flag disables curl's output buffering so you actually see events arrive incrementally instead of all at once at the end. On the receiving end, each event is a chunk of the response as it's produced by the underlying model. Consuming this from Python looks like:
import requests
import json
def stream_flow(message: str):
url = f"{LANGFLOW_URL}/api/v1/run/{FLOW_ID}?stream=true"
payload = {"input_value": message, "output_type": "chat", "input_type": "chat"}
with requests.post(url, json=payload, stream=True, timeout=120) as resp:
for line in resp.iter_lines():
if not line:
continue
decoded = line.decode("utf-8")
if decoded.startswith("data:"):
chunk = json.loads(decoded[len("data:"):].strip())
yield chunk
for event in stream_flow("Explain quantum computing in one paragraph."):
print(event)Streaming adds complexity on both ends — you need to parse partial events correctly, handle connection drops gracefully, and reassemble the final message if you need it as a whole for logging or storage. But for anything user-facing, it's worth the extra plumbing. Users tolerate a slower total response time far better when they can see progress.
Authentication and Locking the Endpoint Down
By default, a freshly installed LangFlow instance may have no authentication at all on its API routes, which is fine for local development and absolutely not fine for anything reachable from the public internet. Before you deploy, generate an API key from the LangFlow settings panel and require it on every request using the x-api-key header, as shown in the examples above.
Beyond the API key, a few practical hardening steps matter once this is a real deployment:
- Put LangFlow behind a reverse proxy (nginx, Caddy, or your cloud provider's load balancer) and terminate TLS there. Never expose the raw LangFlow port directly to the internet.
- Restrict CORS to the specific origins that need to call the API — your own frontend domain, not a wildcard.
- Rate limit at the proxy layer. LangFlow itself doesn't give you fine-grained per-user rate limiting out of the box, so this typically needs to live in front of it — an API gateway, or a simple token-bucket middleware in the backend service that proxies calls to LangFlow.
- Rotate API keys the same way you would any other service credential, and never hardcode them into frontend bundles or commit them to version control.
- Separate environments. Use different LangFlow instances (or at least different flow IDs and API keys) for staging and production so a broken experimental flow never accidentally serves live traffic.
If you're deploying with Docker, the API key and any model provider secrets (OpenAI keys, etc.) should be passed as environment variables, not baked into the image:
docker run -d \
-p 7860:7860 \
-e LANGFLOW_API_KEY="sk-lf-your-api-key-here" \
-e OPENAI_API_KEY="sk-your-openai-key" \
--name langflow-prod \
langflowai/langflow:latestError Handling You'll Actually Hit
A few failure modes come up often enough that it's worth handling them explicitly rather than discovering them in production:
- 422 Unprocessable Entity — usually means your
input_value,input_type, oroutput_typedon't match what the flow expects. Double-check that your flow actually has aChat Input/Chat Outputpair if you're usingchat/chat. - 500 Internal Server Error with a component traceback — one of your nodes failed, often an LLM call that hit a rate limit or an API key that expired. The response body usually includes which component failed; log the full response, not just the status code.
- Timeouts on complex flows — flows with multiple sequential LLM calls or large retrieval steps can legitimately take a while. Set your HTTP client timeout generously (60-120 seconds) rather than assuming anything slower than a few seconds is broken.
- Flow ID not found (404) — this one bites people after they duplicate a flow in the UI for testing. Duplicating creates a new flow ID; if you copy-pasted your production code's flow ID from an old tutorial or an outdated
.envfile, you'll be calling a flow that no longer exists or was deleted.
A defensive wrapper that covers the common cases:
import requests
def safe_run_flow(message: str, session_id: str | None = None) -> str:
payload = {"input_value": message, "output_type": "chat", "input_type": "chat"}
if session_id:
payload["session_id"] = session_id
try:
response = requests.post(
f"{LANGFLOW_URL}/api/v1/run/{FLOW_ID}",
json=payload,
headers={"x-api-key": API_KEY},
timeout=90,
)
response.raise_for_status()
except requests.exceptions.Timeout:
return "The request took too long. Please try again."
except requests.exceptions.HTTPError as e:
return f"LangFlow returned an error: {e.response.status_code}"
try:
data = response.json()
return data["outputs"][0]["outputs"][0]["results"]["message"]["text"]
except (KeyError, IndexError, TypeError):
return "Received an unexpected response format from the flow."Keeping the API Contract Stable While You Iterate on the Flow
One thing that catches teams off guard: if you rename a component, delete a node, or restructure the input/output components inside the visual editor, the API response shape can change even though the flow ID stays the same. If your backend code is reaching several levels deep into the outputs JSON structure, a seemingly cosmetic edit in the LangFlow UI can silently break production.
A few practices reduce this risk:
- Isolate the extraction logic in one function, like the
extract_messagehelper shown earlier, so a shape change only requires updating one place. - Write a small integration test that runs the flow with a fixed input and asserts the output can be parsed, and run it whenever the flow is modified.
- Version your flows. Export the flow JSON after any change you intend to ship and keep it in version control alongside the code that calls it, so you can diff what changed and roll back if a flow edit breaks the API contract.
- Avoid editing production flows directly in the live LangFlow instance. Maintain a staging LangFlow deployment, test flow changes there, and promote the exported flow JSON to production deliberately.
Treating your flow definition with the same discipline you'd apply to application code — version control, staging environment, tests — is the difference between LangFlow feeling like a fragile no-code toy and feeling like a legitimate part of your production stack.
Bringing It All Together
Turning a LangFlow flow into a callable API isn't a separate deployment step bolted on afterward — it's already there, waiting at /api/v1/run/{flow_id}, from the moment you save the flow. The real work is everything around that single endpoint: extracting the answer from a nested response, managing session IDs so conversations don't bleed into each other, using tweaks to avoid duplicating flows for minor variations, streaming responses so users aren't staring at a spinner, and locking the whole thing down with an API key and a reverse proxy before it goes anywhere near production traffic.
None of this requires you to abandon the visual, drag-and-drop appeal that makes LangFlow useful in the first place. It just means treating the flow as a real service boundary: something with a contract, a test, an authentication story, and a rollback plan — the same respect you'd give any other API your application depends on.
If you want to go deeper into building and deploying flows like this — including retrieval-augmented flows, tool-calling agents, and multi-flow orchestration — our LangFlow Tutorial course on teachyou.ai walks through the entire path from first canvas to production endpoint, with the same hands-on approach used in this article.
BootcampA 30-day guided bootcamp: build, harden and ship a production autonomous agent from scratch.
AI AgentsUnderstand how AI agents really work: the loop, the tools, the memory, and why most agent projects fail.
Related reading