# Calling a Workflow Through the API — Dify Workflow Basics

Source: https://www.skillbyai.com/en/dify-workflows/a-api

> App keys, inputs and outputs.

## POST inputs, receive outputs

Each published app has an API with its own **app API key**. For a workflow app you send a POST request to the workflow run endpoint (`/v1/workflows/run`) with `inputs` matching the start node variables, a `response_mode` (`blocking` or `streaming`) and a `user` identifier; the blocking response includes the run id, status and outputs. Chat apps use a chat-messages endpoint with a conversation id. Call the API from your **backend** so the key is never exposed in browsers, handle errors and timeouts, and log the run id. Dify's node names, menus and options change between versions; check the current Dify documentation.

## From editor to integration

Published apps are available as web apps and through an API you can call from any system.

![Three ideas: blocking API calls, streaming, publishing.](assets/figures/dify-workflows/section-7-map.svg) — Figure 7.1 — Blocking calls, streaming and publishing.

## A client tested against a local mock server, run

I ran this with plain Python 3 (scikit-learn 1.9.1 where imported). It models or tests one piece of a Dify app locally; Dify itself was not running. A small client posts inputs to a local server that mimics the workflow-run response shape. With a key starting app- it gets the outputs; with a wrong key it receives HTTP 401. This tests the client code only; check the real request and response fields in Dify's API reference for your version.

```python
import json, threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.request import Request, urlopen

class MockDify(BaseHTTPRequestHandler):      # stands in for a Dify server, for local testing only
    def do_POST(self):
        body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
        ok = self.headers.get("Authorization", "").startswith("Bearer app-")
        reply = {"workflow_run_id": "run-1", "data": {"status": "succeeded",
                 "outputs": {"answer": f"Echo: {body['inputs']['query']}"}}} if ok else {"code": "unauthorized"}
        self.send_response(200 if ok else 401); self.send_header("Content-Type", "application/json"); self.end_headers()
        self.wfile.write(json.dumps(reply).encode())
    def log_message(self, *a): pass

server = HTTPServer(("127.0.0.1", 0), MockDify); threading.Thread(target=server.serve_forever, daemon=True).start()
BASE = f"http://127.0.0.1:{server.server_port}/v1"

def run_workflow(query, api_key):
    payload = {"inputs": {"query": query}, "response_mode": "blocking", "user": "user-123"}
    req = Request(f"{BASE}/workflows/run", data=json.dumps(payload).encode(), method="POST",
                  headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"})
    try:
        with urlopen(req, timeout=10) as r:
            return json.loads(r.read())["data"]["outputs"]
    except Exception as e:
        return f"error: {e}"
print(run_workflow("reset password", "app-demo-key"))
print(run_workflow("reset password", "wrong-key"))
server.shutdown()
```

Output:

```
{'answer': 'Echo: reset password'}
error: HTTP Error 401: Unauthorized
```

## One key per integration

Create separate API keys per calling system so you can rotate or revoke one without breaking the others.

**Quiz:** Where should the Dify app API key be used?

- [ ] In a public JavaScript bundle
- [x] In your backend server, never in browser code
- [ ] In the prompt text
- [ ] In the end node output

*Answer:* In your backend server, never in browser code. Keys in browsers can be stolen.
