Skip to content

n8n Integration

FreeSDN and n8n communicate through two thin, purpose-built primitives - an outbound fabric.webhook step that POSTs Fabric chain data to an n8n webhook URL, and an inbound POST /api/v1/fabric/ingest endpoint that emits a ingest.external event onto the org’s live Fabric bus. The community node n8n-nodes-freesdn wraps both into native n8n trigger and action nodes so you can build the integration without touching JSON or curl.

Neither primitive adds its own runtime. They reuse the same SSRF-guarded HTTP client, org-scoped event bus, and Valkey-backed rate limiter that the rest of the platform uses.

FreeSDN and n8n stay decoupled. There is no long-poll, no synchronous wait, no RPC. A FreeSDN event triggers an outbound POST to n8n. n8n runs its workflow. When n8n is done, it fires an HTTP Request node that POSTs back to FreeSDN. FreeSDN routes the callback through a second Connection. The two sides are independent processes that communicate by plain HTTP.

FreeSDN event
Connection A ── fabric.webhook ──▶ n8n webhook trigger
n8n workflow logic
(branch, retry, call APIs, wait …)
HTTP Request node
POST /api/v1/fabric/ingest
Connection B ◀── ingest.external ───────┘
Fabric step (notify, store_blob, vm.snapshot, …)

You can run Connection A alone (fire-and-forget to n8n), Connection B alone (n8n initiates something in FreeSDN), or both together for a full bidirectional round-trip.

n8n-nodes-freesdn is a community node that adds two items to the n8n palette:

  • FreeSDN Trigger - receives an outbound fabric.webhook POST from a FreeSDN Connection and starts your n8n workflow. It handles HMAC signature verification for you.
  • FreeSDN (action node) - select Resource → Fabric (Catalog Op / Event), then Fabric Action → Emit Event, and enter the name and payload to call POST /api/v1/fabric/ingest and push a result back into the Fabric bus.

Install it from the n8n community nodes directory (search for n8n-nodes-freesdn) or via npm in a self-hosted instance:

Terminal window
npm install n8n-nodes-freesdn

After installation, restart n8n and the two node types appear in the palette under the FreeSDN category.

n8n authenticates to /fabric/ingest using an organization API key sent as the X-API-Key header. Create one in Settings → API Keys. The key must include the event:write scope. A read-only or narrowly scoped key returns 403.

Create a second API key for Connection authoring

Section titled “Create a second API key for Connection authoring”

Creating, updating, and deleting Fabric Connections requires an unscoped org-admin credential - either a browser session JWT or an API key with no scope list set. An API key that carries any explicit scope (including event:write) is treated as a deliberately-narrowed credential and is refused with 403 on all Connection CRUD endpoints.

Create a second key in Settings → API Keys, leave the Scopes field empty, and store it separately (for example as ADMIN_KEY in your shell). This key is for administrative authoring only - it is not sent to n8n and does not need to be placed in n8n’s Credentials store.

Section titled “Enable HMAC signature verification (strongly recommended)”

When FreeSDN sends a webhook to n8n, it can sign the body so n8n can verify the POST originated from your FreeSDN instance and was not tampered with in transit.

FreeSDN has two delivery paths to n8n, and each uses different signature headers:

The FreeSDN Trigger node uses FreeSDN’s webhook delivery system. When a delivery arrives, FreeSDN signs it with these headers:

HeaderValue
X-Webhook-Signaturesha256=<HMAC-SHA256 of "{X-Webhook-Timestamp}.{body}"> - the timestamp value, a literal dot, then the raw body bytes
X-Webhook-TimestampUnix timestamp; bound into the signed content to prevent replay

In the FreeSDN API credential, fill in the Webhook Signing Secret field with a strong random value (at least 32 characters). The Trigger node reads X-Webhook-Signature and X-Webhook-Timestamp, verifies the signature on every incoming request, and rejects requests with a missing, invalid, or replayed signature. No manual Code node is needed - verification is built into the trigger.

Raw Webhook node + fabric.webhook step path

Section titled “Raw Webhook node + fabric.webhook step path”

When a fabric.webhook Fabric step POSTs directly to a standard n8n Webhook node (bypassing the community node), a different set of headers is used. Set the signing secret on the FreeSDN API container and restart:

Terminal window
# Set in the API container environment, then restart.
FABRIC_WEBHOOK_SIGNING_SECRET=<at-least-32-random-chars>

When this variable is non-empty, every fabric.webhook POST carries two additional headers:

HeaderValue
X-Fabric-Signaturesha256=<HMAC-SHA256 of "{X-Fabric-Timestamp}.{body}"> - the timestamp value, a literal dot, then the raw body bytes
X-Fabric-TimestampUnix timestamp; bound into the signed content to prevent replay

Verify the signature yourself in a Code node immediately after the Webhook trigger:

// Code node - verify X-Fabric-Signature (Stripe-style timestamp binding)
const crypto = require('crypto');
const secret = '<your signing secret>';
const body = $input.first().json.body; // raw body string (Webhook node → Options → "Raw Body" must be ON)
const ts = $input.first().json.headers['x-fabric-timestamp'];
const sig = $input.first().json.headers['x-fabric-signature'];
// The signed content is "{timestamp}.{body}" - timestamp bound to prevent replay
const signed = ts + '.' + body;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(signed)
.digest('hex');
if (sig !== expected) {
throw new Error('Signature mismatch - reject request');
}
return $input.all();

Allow your n8n host if it is on a private network

Section titled “Allow your n8n host if it is on a private network”

fabric.webhook passes through the platform’s SSRF guard. Public URLs (n8n Cloud, a public server) work without any configuration. If your n8n is on a LAN IP or a Tailscale tailnet, add it to the server-side allowlist:

Terminal window
# Set in the API container environment, then restart.
FABRIC_WEBHOOK_ALLOWED_HOSTS=n8n.example.net,192.168.1.150

This bypasses the private-IP block only for the listed hostnames or IPs. Even allow-listed hosts remain DNS-pinned and TLS-verified. Cloud-metadata addresses (169.254.169.254, fd00:ec2::254, 100.100.100.200) are never reachable regardless of this setting. Private-network and CGNAT ranges (including Tailscale 100.64.0.0/10 addresses) are blocked by default but can be reached by explicitly listing the host in FABRIC_WEBHOOK_ALLOWED_HOSTS.

Connection A fires on a FreeSDN event and sends the chain data to your n8n webhook URL.

Using the FreeSDN Trigger node (community node path)

Section titled “Using the FreeSDN Trigger node (community node path)”
  1. In n8n, create a new workflow and add a FreeSDN Trigger node.
  2. In the node’s credentials, paste the Signing Secret (any random string you choose) into the Webhook Secret field. FreeSDN will sign each delivery with this secret.
  3. Select the events you want to receive (for example cameras.event.motion).
  4. Activate the workflow. The FreeSDN Trigger node automatically registers a webhook with your FreeSDN instance (via POST /api/v1/webhooks) and configures it to forward the selected events. No manual Fabric Connection is required.
Terminal window
# Requires an unscoped org-admin API key - not the event:write key.
curl -sX POST https://<freesdn>/api/v1/fabric/connections \
-H "X-API-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Motion alert → n8n",
"source_event": "cameras.event.motion",
"cooldown_seconds": 30,
"steps": [
{
"operation_id": "fabric.webhook",
"params": {
"url": "https://n8n.example.com/webhook/freesdn-motion",
"payload": {
"event_type": "{{trigger.event_type}}",
"camera_id": "{{trigger.camera_id}}",
"site_id": "{{trigger.site_id}}"
},
"headers": {
"Content-Type": "application/json"
}
}
}
]
}'

{{trigger}} in a payload value sends the full event payload. {{trigger.<field>}} pulls a single field. Omit payload entirely to forward the raw trigger event as-is. See Connections - Templating for the full syntax.

Event typeWhen it firesPayload highlights
cameras.event.motionCamera motion detectedcamera_id, site_id
cameras.event.personPerson detectedcamera_id, site_id
controller.change.appliedA staged write was applied to a devicevendor, feature, change_id, actor_id
controller.change.failedA staged write failedvendor, feature, change_id
device.status.changedA device went online or offlinedevice_id, data.old_status, data.new_status
device.discoveredA new device was adopteddevice_id, site_id
storage.pool.degradedZFS pool degradedpool details
backup.validation.failedBackup validation failedbackup metadata
gateway.brain.offlineFirewall brain went offlinesite details
ai.budget.warningAI assistant nearing budget limitbudget details

Browse the full list and their payload schemas in Fabric → Catalog → Events.

Your n8n workflow starts with the FreeSDN Trigger node (or a standard Webhook node). From there, use any n8n nodes you need:

  1. Parse and branch on fields from the FreeSDN payload.
  2. Call external APIs - Slack, PagerDuty, a ticketing system, a third-party AI service.
  3. Wait for human approval using n8n’s Wait node.
  4. Transform data before sending it back.
  5. Post back to FreeSDN using the FreeSDN node (Resource → Fabric → Emit Event) or an HTTP Request node.

The HTTP Request node that posts back to FreeSDN:

SettingValue
MethodPOST
URLhttps://<freesdn>/api/v1/fabric/ingest
AuthenticationHeader Auth, name X-API-Key, value <your org API key>
BodyJSON - see below
{
"name": "motion_reviewed",
"payload": {
"camera_id": "{{ $json.camera_id }}",
"verdict": "{{ $json.verdict }}"
}
}

The name field is your routing label. FreeSDN sanitizes it (lowercases, strips [^a-z0-9_-], caps at 64 characters). Use a distinct name for each callback flow so Connection B can filter on it.

Set up Connection B - n8n result to a FreeSDN action

Section titled “Set up Connection B - n8n result to a FreeSDN action”

Connection B listens for ingest.external events and filters on the name field to route different callbacks to different actions.

Using the FreeSDN node (community node path)

Section titled “Using the FreeSDN node (community node path)”

In the n8n workflow, at the end of your logic, add a FreeSDN node. Set Resource to Fabric (Catalog Op / Event), set Fabric Action to Emit Event, set your FreeSDN instance URL, paste the org API key from the Credentials store, and enter the name and payload. The node POSTs to /fabric/ingest and surfaces the 202 response.

Terminal window
# Requires an unscoped org-admin API key - not the event:write key.
curl -sX POST https://<freesdn>/api/v1/fabric/connections \
-H "X-API-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "n8n motion verdict → notify team",
"source_event": "ingest.external",
"conditions": {
"logic": "and",
"conditions": [
{"field": "name", "operator": "eq", "value": "motion_reviewed"}
]
},
"steps": [
{
"operation_id": "fabric.notify",
"params": {
"channels": {"email": ["security@example.com"]},
"title": "Motion reviewed",
"body": "Verdict: {{trigger.data.verdict}} - camera {{trigger.data.camera_id}}"
}
}
]
}'

Route multiple callback names from the same n8n workflow by creating one Connection B per name value, each pointing at a different Fabric step.

PropertyValue
PathPOST /api/v1/fabric/ingest
AuthenticationX-API-Key: <org api key> with event:write scope
Emitted event typeAlways ingest.external - never caller-controlled
Body fieldsname (optional, ≤64 chars) and payload (any JSON object)
Payload cap64 KiB (JSON-serialized payload field only) - returns 413 if exceeded; the name field is validated separately by Pydantic (max 64 chars)
Per-org rate limit120 requests per 60-second window (cluster-wide via Valkey)
Success response202 {"accepted": true, "event_type": "ingest.external", "name": "<name>"}

The event type is always ingest.external. An external caller cannot inject a native event type (device.status.changed, cameras.event.motion, or any other native type) into the bus by setting a body field. Org ownership is derived from the authenticated API key, never from the request body.

Test Connection A (outbound) - point fabric.webhook at a public echo service and hit Test in the Fabric connection builder:

Terminal window
# Create a test connection pointing at httpbin
# Requires an unscoped org-admin API key - not the event:write key.
curl -sX POST https://<freesdn>/api/v1/fabric/connections \
-H "X-API-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Test outbound webhook",
"source_event": "ingest.external",
"conditions": {"logic": "and", "conditions": [
{"field": "name", "operator": "eq", "value": "test_ping"}
]},
"steps": [
{
"operation_id": "fabric.webhook",
"params": {"url": "https://httpbin.org/post"}
}
]
}'
# Then fire it
curl -sX POST https://<freesdn>/api/v1/fabric/ingest \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"name": "test_ping", "payload": {"hello": "world"}}'

Check Fabric → Connections → Runs to see the webhook response echoed back.

Test Connection B (inbound) - simulate an n8n callback directly:

Terminal window
curl -sX POST https://<freesdn>/api/v1/fabric/ingest \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"name": "motion_reviewed", "payload": {"verdict": "false_positive"}}'
# → 202 {"accepted": true, "event_type": "ingest.external", "name": "motion_reviewed"}

Open Fabric → Connections → Runs on Connection B to confirm it fired and inspect the step outputs.

Test the HMAC signature - verify the signature header is present when the signing secret is set:

Terminal window
curl -si -X POST https://httpbin.org/post \
-H "X-Fabric-Signature: sha256=<value>" \
-H "X-Fabric-Timestamp: <unix>" \
-H "Content-Type: application/json" \
-d '{"test": true}'

Alternatively, fire the test connection above and check the recorded headers in the run output - X-Fabric-Signature appears in the step output when the signing secret is configured.

ConcernProtection
SSRF on outbound webhookPublic-only by default; private/LAN hosts require explicit FABRIC_WEBHOOK_ALLOWED_HOSTS; cloud-metadata never reachable
Outbound body tamperingHMAC-SHA256 X-Fabric-Signature + timestamp bound when FABRIC_WEBHOOK_SIGNING_SECRET is set
AI assistant calling fabric.webhookExcluded from the AI bridge - the assistant cannot auto-POST org data to an arbitrary URL
Inbound authenticationevent:write scope required; read-only keys refused with 403
Inbound event type spoofingEvent type is always ingest.external; caller cannot set it to a native type
Inbound org isolationOrg taken from the authenticated API key, never from the request body
Inbound payload size64 KiB cap on the JSON-serialized payload field; 413 if exceeded (name field capped separately by Pydantic at 64 chars)
Inbound rate limiting120 per 60-second window per org, cluster-wide via Valkey
Write safetyWrite ops triggered by inbound callback always stage - never auto-applied
Step param confidentialityNon-author org members see step params replaced with {"__redacted__": "hidden - author-only"}

The same two primitives - fabric.webhook and POST /fabric/ingest - work identically with Zapier, Make, Node-RED, Home Assistant, or any system that can send and receive HTTP. The n8n-nodes-freesdn community node adds the native FreeSDN Trigger and FreeSDN nodes to n8n specifically for a no-code experience, but there is nothing n8n-specific about the underlying API.

  • Connections - full Connection authoring reference, templating syntax, staged-write model
  • The Fabric - catalog vocabulary, built-in sinks, executor model, env vars
  • Plugin System - declare custom operations and events via SDK plugins
  • Marketplace - install signed community plugins