n8n REST API: Trigger Workflows Programmatically in 2026

Use the n8n REST API to trigger workflows via webhook and API calls. Covers auth, payloads, error handling, and real Python/Node examples.

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 → AuthenticationHeader 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 to Immediately and 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 /run for 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