Problem: You Need to Trigger n8n Workflows From Your Own Code
n8n's visual editor is great for building workflows. But when you need to fire them from an external service, a cron job, or your own backend, you need the REST API — and the docs aren't always clear on the right approach.
You'll learn:
- The difference between n8n's Webhook trigger and its built-in REST API
- How to authenticate and call both correctly
- How to pass data in the request body and read it inside the workflow
- Error handling patterns for production use
Time: 20 min | Difficulty: Intermediate
Why There Are Two Ways to Trigger a Workflow
n8n gives you two distinct mechanisms for external triggering. Picking the wrong one is the most common source of confusion.
Webhook Trigger node — a URL that activates a specific workflow. This is the right tool for 90% of use cases. It's simpler, supports any HTTP client, and lets you shape the response the workflow returns.
n8n REST API (/api/v1/workflows/:id/execute) — a management API protected by an API key. Use this when you need to trigger workflows from a privileged backend and don't want to expose a public webhook URL.
Both approaches are covered here.
Prerequisites
- n8n running locally or on a server (v1.x — this guide targets n8n 1.30+)
- Basic familiarity with HTTP requests
- Python 3.11+ or Node.js 20+ for the code examples
Solution
Step 1: Create a Workflow With a Webhook Trigger
In the n8n editor, create a new workflow and add a Webhook node as the trigger.
Set these options:
- HTTP Method:
POST - Path: something memorable, e.g.
send-email - Response Mode:
Last Node(returns the final node's output as the HTTP response) - Authentication:
Header Auth(add this in production — see Step 3)
Click Listen For Test Event and note the test URL shown:
http://localhost:5678/webhook-test/send-email
The production URL (used after activation) drops the -test segment:
http://localhost:5678/webhook/send-email
Critical: the /webhook-test/ URL only works while the editor is open and listening. For production calls, always use /webhook/ against an activated workflow.
Step 2: Read the Incoming Payload in Your Workflow
After the Webhook node, add any downstream node (e.g. Send Email, HTTP Request, Set).
The incoming request body is available at:
{{ $json.body.yourField }}
For example, if you POST this payload:
{
"to": "user@example.com",
"subject": "Hello from the API"
}
Reference it in downstream nodes as {{ $json.body.to }} and {{ $json.body.subject }}.
Query parameters land at {{ $json.query.paramName }} and headers at {{ $json.headers["x-custom-header"] }}.
Step 3: Add Header Authentication to the Webhook
Without auth, anyone with the URL can trigger your workflow. Add a shared secret.
In the Webhook node → Authentication → Header Auth:
- Name:
x-webhook-secret - Value: a long random string (generate one:
openssl rand -hex 32)
Store this secret in your calling application's environment variables, never in source code.
Step 4: Trigger the Webhook From Python
import os
import httpx # pip install httpx
N8N_WEBHOOK_URL = os.environ["N8N_WEBHOOK_URL"] # https://your-n8n.com/webhook/send-email
N8N_SECRET = os.environ["N8N_WEBHOOK_SECRET"]
def trigger_send_email(to: str, subject: str) -> dict:
response = httpx.post(
N8N_WEBHOOK_URL,
json={"to": to, "subject": subject},
headers={"x-webhook-secret": N8N_SECRET},
timeout=30, # n8n workflows can be slow if they call external APIs
)
# n8n returns 200 on success, 404 if workflow not found/inactive
response.raise_for_status()
return response.json()
if __name__ == "__main__":
result = trigger_send_email("user@example.com", "Hello from the API")
print(result)
Expected output:
[{"json": {"success": true}}]
n8n wraps the last node's output in a [{"json": {...}}] envelope. Parse accordingly.
If it fails:
404 Not Found→ Workflow is not activated. Click the toggle in the n8n editor.401 Unauthorized→ Secret mismatch. Double-check the header name matches exactly.timeout→ Increase timeout or switch Response Mode toImmediatelyand handle async.
Step 5: Trigger the Webhook From Node.js / TypeScript
const N8N_WEBHOOK_URL = process.env.N8N_WEBHOOK_URL!;
const N8N_SECRET = process.env.N8N_WEBHOOK_SECRET!;
interface TriggerPayload {
to: string;
subject: string;
}
async function triggerWorkflow(payload: TriggerPayload): Promise<unknown> {
const res = await fetch(N8N_WEBHOOK_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-webhook-secret": N8N_SECRET,
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(30_000), // 30s — matches httpx example above
});
if (!res.ok) {
throw new Error(`n8n webhook failed: ${res.status} ${await res.text()}`);
}
return res.json();
}
// Usage
const result = await triggerWorkflow({ to: "user@example.com", subject: "Hello" });
console.log(result);
Step 6: Use the n8n REST API for Privileged Execution
If you want to trigger workflows without a Webhook node — or from a trusted internal backend only — use n8n's management API instead.
Generate an API key:
In n8n → Settings → n8n API → Create API Key. Copy it — it's shown only once.
Trigger a workflow by ID:
# Replace WORKFLOW_ID with the numeric ID from the workflow URL
curl -X POST https://your-n8n.com/api/v1/workflows/WORKFLOW_ID/run \
-H "X-N8N-API-KEY: your-api-key" \
-H "Content-Type: application/json" \
-d '{"startNodes": [], "runData": {}}'
Python equivalent:
import os
import httpx
N8N_BASE_URL = os.environ["N8N_BASE_URL"] # https://your-n8n.com
N8N_API_KEY = os.environ["N8N_API_KEY"]
WORKFLOW_ID = os.environ["N8N_WORKFLOW_ID"] # numeric string, e.g. "42"
def run_workflow_via_api() -> dict:
response = httpx.post(
f"{N8N_BASE_URL}/api/v1/workflows/{WORKFLOW_ID}/run",
headers={"X-N8N-API-KEY": N8N_API_KEY},
json={"startNodes": [], "runData": {}},
timeout=60,
)
response.raise_for_status()
return response.json()
Important: The REST API does not support passing a custom payload body the way a Webhook does. If your workflow needs input data, use the Webhook approach (Steps 1–5) instead.
Step 7: Handle Async Workflows (Long-Running)
By default, the Webhook node holds the HTTP connection open until the last node finishes. If your workflow calls a slow LLM or scrapes a site, this can hit client timeouts.
Switch to async by changing Response Mode to Immediately:
The Webhook node instantly returns {"message": "Workflow was started"} and runs the rest in the background.
Your caller then either polls for results or you push results back via a second webhook or a message queue.
# Fire-and-forget pattern — don't wait for the full result
response = httpx.post(
N8N_WEBHOOK_URL,
json=payload,
headers={"x-webhook-secret": N8N_SECRET},
timeout=5, # Only waiting for acknowledgment, not completion
)
response.raise_for_status()
# {"message": "Workflow was started"}
print("Workflow triggered:", response.json())
Verification
Activate your workflow, then run:
curl -X POST https://your-n8n.com/webhook/send-email \
-H "Content-Type: application/json" \
-H "x-webhook-secret: YOUR_SECRET" \
-d '{"to": "test@example.com", "subject": "Verify it works"}'
You should see: a 200 response with the last node's output as JSON.
In the n8n editor, check Executions in the left sidebar. A successful run appears as a green row. Click it to inspect every node's input and output — this is the fastest way to debug payload mapping issues.
What You Learned
- Use Webhook trigger for data-driven invocations from any HTTP client
- Use REST API
/runfor privileged, no-payload executions from internal services - Always protect webhooks with header auth in production
- Switch to Response Mode: Immediately when workflows exceed ~10 seconds
- n8n wraps all webhook responses in
[{"json": {...}}]— unwrap before using
Limitation: The n8n REST API's /run endpoint doesn't support custom input data. For anything that needs runtime parameters, a Webhook trigger is the correct abstraction.
Tested on n8n 1.30.1, self-hosted via Docker, Ubuntu 24.04 and macOS 15