How to integrate any Dify app — chatbot, workflow, or agent — into external applications
Why the Dify API matters
Dify's visual editor is great for building, but the real value is embedding those apps into your own products. Dify exposes every app type — chatbot, workflow, agent — as a REST API. You build in Dify, call it from anywhere.
This guide covers authentication, the four API types, streaming vs blocking responses, conversation persistence, error handling, and rate limiting.
Authentication
Every API call requires a Bearer token. Each Dify app has its own API key — find it under your app's API Access settings.
# All requests use this header
Authorization: Bearer YOUR_APP_API_KEY
API keys are per-app, not per-account. If you have five Dify apps, you have five different API keys. Do not confuse them.The four API types
| App type | Endpoint prefix | Use case |
|---|---|---|
| Chatbot / Chatflow | /v1/chat-messages | Conversational back-and-forth |
| Completion (legacy) | /v1/completion-messages | Single-turn text generation |
| Workflow | /v1/workflows/run | Run a Workflow app |
| Agent | /v1/chat-messages | Same endpoint as Chatbot |
Chat API: blocking vs streaming
The most common call. Use response_mode: blocking for simple integrations and response_mode: streaming for real-time UIs.
Blocking call
import requests
API_KEY = 'app-xxxxxxxxxxxx'
BASE_URL = 'https://api.dify.ai/v1'
def chat(message: str, conversation_id: str = '') -> dict:
resp = requests.post(
f'{BASE_URL}/chat-messages',
headers={'Authorization': f'Bearer {API_KEY}'},
json={
'inputs': {},
'query': message,
'response_mode': 'blocking',
'conversation_id': conversation_id,
'user': 'user-123',
}
)
resp.raise_for_status()
data = resp.json()
return {
'answer': data['answer'],
'conversation_id': data['conversation_id'],
}
# First turn
result = chat('What is the refund policy?')
print(result['answer'])
# Continue the conversation
result2 = chat('What about international orders?', result['conversation_id'])
Streaming call
import requests
import json
def chat_stream(message: str, conversation_id: str = ''):
"""Yields text chunks as they arrive."""
resp = requests.post(
f'{BASE_URL}/chat-messages',
headers={'Authorization': f'Bearer {API_KEY}'},
json={
'inputs': {},
'query': message,
'response_mode': 'streaming',
'conversation_id': conversation_id,
'user': 'user-123',
},
stream=True
)
resp.raise_for_status()
for line in resp.iter_lines():
if line and line.startswith(b'data: '):
payload = json.loads(line[6:])
event = payload.get('event')
if event == 'message':
yield payload.get('answer', '')
elif event == 'message_end':
return
for chunk in chat_stream('Explain quantum entanglement simply.'):
print(chunk, end='', flush=True)
Workflow API
Running a Workflow app is slightly different — you pass named inputs instead of a query string.
def run_workflow(inputs: dict) -> dict:
resp = requests.post(
f'{BASE_URL}/workflows/run',
headers={'Authorization': f'Bearer {API_KEY}'},
json={
'inputs': inputs, # matches your Workflow's input variables
'response_mode': 'blocking',
'user': 'user-123',
}
)
resp.raise_for_status()
return resp.json()['data']['outputs']
result = run_workflow({'document_url': 'https://example.com/contract.pdf'})
print(result['summary'])
Conversation persistence
For chatbot/agent apps, Dify automatically maintains conversation history server-side. Your only job is to pass the conversation_id back on every subsequent turn.
class DifyChat:
def __init__(self, user_id: str):
self.user_id = user_id
self.conversation_id = ''
def send(self, message: str) -> str:
result = chat(message, self.conversation_id)
self.conversation_id = result['conversation_id'] # persist
return result['answer']
session = DifyChat('user-456')
print(session.send('Hi, I need help with my order'))
print(session.send('Order number is 12345')) # context preserved
Error handling
| HTTP status | Meaning | What to do |
|---|---|---|
| 400 | Invalid request body | Check your inputs dict matches the app's variables |
| 401 | Bad API key | Re-check the key — it is per-app, not per-account |
| 404 | App not found | Confirm the app is published, not just saved as draft |
| 429 | Rate limited | Back off with exponential retry; consider caching responses |
| 500 | Dify server error | Retry once; if persistent, check Dify status page |
import time
def chat_with_retry(message: str, conversation_id: str = '', max_retries: int = 3) -> dict:
for attempt in range(max_retries):
try:
return chat(message, conversation_id)
except requests.HTTPError as e:
if e.response.status_code == 429:
wait = 2 ** attempt
print(f'Rate limited, waiting {wait}s...')
time.sleep(wait)
elif e.response.status_code >= 500:
time.sleep(1)
else:
raise # 400/401/404 are not retryable
raise RuntimeError('Max retries exceeded')
Rate limits and multi-tenant patterns
Dify Cloud enforces rate limits per API key. For multi-tenant apps where each customer should be isolated, pass a unique user parameter per request — Dify uses this for conversation isolation and the analytics dashboard.
The user field in the request body is your tenant/user identifier. It does not affect the LLM call but is used by Dify's analytics and helps you audit usage per customer in the Logs tab.Self-hosted rate limiting
On self-hosted Dify you can configure Nginx rate limiting upstream. The Dify app itself does not enforce rate limits on self-hosted deployments by default.
Passing file inputs
Some Dify apps accept file uploads. You can pass files as base64 or upload them first with the file upload endpoint.
# Upload a file first
def upload_file(path: str) -> str:
with open(path, 'rb') as f:
resp = requests.post(
f'{BASE_URL}/files/upload',
headers={'Authorization': f'Bearer {API_KEY}'},
files={'file': f},
data={'user': 'user-123'}
)
resp.raise_for_status()
return resp.json()['id']
# Then reference the file ID in your chat or workflow call
file_id = upload_file('contract.pdf')
chat_with_file = {
'inputs': {},
'query': 'Summarise this contract',
'files': [{'type': 'document', 'transfer_method': 'local_file', 'upload_file_id': file_id}],
'response_mode': 'blocking',
'user': 'user-123',
}
Quick checklist
- API key is per-app — check you are using the right one
- Use response_mode: streaming for any user-facing UI
- Store and pass conversation_id on every turn for chat apps
- The user field is your tenant identifier — make it unique per customer
- Publish your app (not just save) before the API will respond
- Handle 429 with exponential backoff