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.
How the integration works
Section titled “How the integration works”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.
The community node: n8n-nodes-freesdn
Section titled “The community node: n8n-nodes-freesdn”n8n-nodes-freesdn is a community node that adds two items to the n8n palette:
- FreeSDN Trigger - receives an outbound
fabric.webhookPOST 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
nameandpayloadto callPOST /api/v1/fabric/ingestand 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:
npm install n8n-nodes-freesdnAfter installation, restart n8n and the two node types appear in the palette under the FreeSDN category.
Prerequisites
Section titled “Prerequisites”Create an org API key for ingest
Section titled “Create an org API key for ingest”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.
Enable HMAC signature verification (strongly recommended)
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:
FreeSDN Trigger community node path
Section titled “FreeSDN Trigger community node path”The FreeSDN Trigger node uses FreeSDN’s webhook delivery system. When a delivery arrives, FreeSDN signs it with these headers:
| Header | Value |
|---|---|
X-Webhook-Signature | sha256=<HMAC-SHA256 of "{X-Webhook-Timestamp}.{body}"> - the timestamp value, a literal dot, then the raw body bytes |
X-Webhook-Timestamp | Unix 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:
# 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:
| Header | Value |
|---|---|
X-Fabric-Signature | sha256=<HMAC-SHA256 of "{X-Fabric-Timestamp}.{body}"> - the timestamp value, a literal dot, then the raw body bytes |
X-Fabric-Timestamp | Unix 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 replayconst 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:
# Set in the API container environment, then restart.FABRIC_WEBHOOK_ALLOWED_HOSTS=n8n.example.net,192.168.1.150This 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.
Set up Connection A - FreeSDN to n8n
Section titled “Set up Connection A - FreeSDN to n8n”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)”- In n8n, create a new workflow and add a FreeSDN Trigger node.
- 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.
- Select the events you want to receive (for example
cameras.event.motion). - 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.
Using the raw API
Section titled “Using the raw API”# 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.
Useful source events
Section titled “Useful source events”| Event type | When it fires | Payload highlights |
|---|---|---|
cameras.event.motion | Camera motion detected | camera_id, site_id |
cameras.event.person | Person detected | camera_id, site_id |
controller.change.applied | A staged write was applied to a device | vendor, feature, change_id, actor_id |
controller.change.failed | A staged write failed | vendor, feature, change_id |
device.status.changed | A device went online or offline | device_id, data.old_status, data.new_status |
device.discovered | A new device was adopted | device_id, site_id |
storage.pool.degraded | ZFS pool degraded | pool details |
backup.validation.failed | Backup validation failed | backup metadata |
gateway.brain.offline | Firewall brain went offline | site details |
ai.budget.warning | AI assistant nearing budget limit | budget details |
Browse the full list and their payload schemas in Fabric → Catalog → Events.
Build the n8n workflow
Section titled “Build the n8n workflow”Your n8n workflow starts with the FreeSDN Trigger node (or a standard Webhook node). From there, use any n8n nodes you need:
- Parse and branch on fields from the FreeSDN payload.
- Call external APIs - Slack, PagerDuty, a ticketing system, a third-party AI service.
- Wait for human approval using n8n’s Wait node.
- Transform data before sending it back.
- 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:
| Setting | Value |
|---|---|
| Method | POST |
| URL | https://<freesdn>/api/v1/fabric/ingest |
| Authentication | Header Auth, name X-API-Key, value <your org API key> |
| Body | JSON - 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.
Using the raw API
Section titled “Using the raw API”# 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.
Inbound endpoint reference
Section titled “Inbound endpoint reference”| Property | Value |
|---|---|
| Path | POST /api/v1/fabric/ingest |
| Authentication | X-API-Key: <org api key> with event:write scope |
| Emitted event type | Always ingest.external - never caller-controlled |
| Body fields | name (optional, ≤64 chars) and payload (any JSON object) |
| Payload cap | 64 KiB (JSON-serialized payload field only) - returns 413 if exceeded; the name field is validated separately by Pydantic (max 64 chars) |
| Per-org rate limit | 120 requests per 60-second window (cluster-wide via Valkey) |
| Success response | 202 {"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 the integration without n8n
Section titled “Test the integration without n8n”Test Connection A (outbound) - point fabric.webhook at a public echo service and hit Test in the Fabric connection builder:
# 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 itcurl -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:
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:
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.
Security model
Section titled “Security model”| Concern | Protection |
|---|---|
| SSRF on outbound webhook | Public-only by default; private/LAN hosts require explicit FABRIC_WEBHOOK_ALLOWED_HOSTS; cloud-metadata never reachable |
| Outbound body tampering | HMAC-SHA256 X-Fabric-Signature + timestamp bound when FABRIC_WEBHOOK_SIGNING_SECRET is set |
| AI assistant calling fabric.webhook | Excluded from the AI bridge - the assistant cannot auto-POST org data to an arbitrary URL |
| Inbound authentication | event:write scope required; read-only keys refused with 403 |
| Inbound event type spoofing | Event type is always ingest.external; caller cannot set it to a native type |
| Inbound org isolation | Org taken from the authenticated API key, never from the request body |
| Inbound payload size | 64 KiB cap on the JSON-serialized payload field; 413 if exceeded (name field capped separately by Pydantic at 64 chars) |
| Inbound rate limiting | 120 per 60-second window per org, cluster-wide via Valkey |
| Write safety | Write ops triggered by inbound callback always stage - never auto-applied |
| Step param confidentiality | Non-author org members see step params replaced with {"__redacted__": "hidden - author-only"} |
Other automation tools
Section titled “Other automation tools”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.
Next steps
Section titled “Next steps”- 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