A lightweight Rust service that enables multiple n8n workflows to share a single inbound webhook for Slack, Jira, and GitHub. It receives all events at one endpoint per service and intelligently routes them to matching n8n workflows based on their trigger configurations.
n8n's built-in trigger nodes (Slack Trigger, Jira Trigger, GitHub Trigger) create unique webhooks for each workflow. External services like Slack, Jira, and GitHub only support a single event subscription URL per app/instance, which forces organizations to:
- Slack: Create separate Slack apps for each workflow, manage multiple OAuth credentials, and deal with complex app approval processes
- Jira: Register separate webhooks in Jira for each workflow, leading to webhook sprawl and management overhead
- GitHub: Register separate webhooks per repository per workflow, causing webhook sprawl across repositories
This is administratively unworkable for organizations with multiple event-triggered workflows.
Unihook acts as a router between external services and n8n:
- Single Webhook: Register one URL per service (Slack, Jira, GitHub)
- Dynamic Discovery: Automatically discovers n8n workflows with matching triggers via the n8n API
- Smart Routing: Forwards events only to workflows whose trigger configuration matches the event
- Zero Execution Waste: Events that don't match any trigger never reach n8n
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Slack Events │────▶│ │────▶│ n8n Workflow A │
│ API │ │ │────▶│ n8n Workflow B │
└─────────────────┘ │ │ └─────────────────┘
│ │
┌─────────────────┐ │ Unihook │ ┌─────────────────┐
│ Jira Webhooks │────▶│ Router │────▶│ n8n Workflow C │
│ │ │ │────▶│ n8n Workflow D │
└─────────────────┘ │ │ └─────────────────┘
│ │
┌─────────────────┐ │ │ ┌─────────────────┐
│ GitHub Webhooks │────▶│ │────▶│ n8n Workflow E │
│ │ │ │────▶│ n8n Workflow F │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│
▼
┌──────────────────┐
│ n8n API │
│ (discover │
│ triggers) │
└──────────────────┘
- Docker and Docker Compose (recommended)
- OR Rust 1.86+ for local development
- n8n instance with API access enabled
- n8n API key
- Clone the repository:
git clone https://github.com/your-org/n8n-unihook.git
cd n8n-unihook- Create a
.envfile:
# Required
N8N_API_KEY=your-n8n-api-key
# Optional (defaults shown)
N8N_API_URL=http://n8n:5678
REFRESH_INTERVAL_SECS=60
RUST_LOG=n8n_slack_unihook=info
# Inbound webhook signature verification (optional but recommended for GitHub)
# Set this to the shared secret configured in your GitHub webhook settings
# GITHUB_WEBHOOK_SECRET=your-github-webhook-secret- Start the service:
docker-compose up -ddocker build -t n8n-unihook .
docker run -d \
--name n8n-unihook \
-p 3000:3000 \
-e N8N_API_KEY=your-n8n-api-key \
-e N8N_API_URL=http://your-n8n-host:5678 \
n8n-unihook# Set environment variables
export N8N_API_KEY=your-n8n-api-key
export N8N_API_URL=http://localhost:5678
# Build and run
cargo run| Environment Variable | Required | Default | Description |
|---|---|---|---|
N8N_API_KEY |
Yes | - | Your n8n API key |
N8N_API_URL |
No | http://localhost:5678 |
n8n instance URL |
LISTEN_ADDR |
No | 0.0.0.0:3000 |
Address to bind the HTTP server |
REFRESH_INTERVAL_SECS |
No | 60 |
How often to refresh trigger configs |
N8N_ENDPOINT_WEBHOOK |
No | webhook |
n8n production webhook path segment |
N8N_ENDPOINT_WEBHOOK_TEST |
No | webhook-test |
n8n test webhook path segment |
GITHUB_WEBHOOK_SECRET |
No | - | Shared secret for verifying inbound GitHub webhooks (HMAC-SHA256 via X-Hub-Signature-256) |
RUST_LOG |
No | n8n_slack_unihook=info |
Log level |
-
Create or configure your Slack App at api.slack.com/apps
-
Enable Event Subscriptions:
- Go to "Event Subscriptions"
- Toggle "Enable Events" to On
- Set the Request URL to:
https://your-domain.com/slack/events - Slack will send a verification challenge — Unihook handles this automatically
-
Subscribe to Events:
- Add the bot events your workflows need:
message.channels— New messages in public channelsapp_mention— When your bot is @mentionedreaction_added— Reactions added to messagesfile_shared— Files shared in channelschannel_created— New channels createdteam_join— New users joining the workspace
- Add the bot events your workflows need:
-
Install the App to your workspace
Unihook queries the n8n API to discover workflows with Slack Trigger nodes. For each trigger, it extracts:
- Event type (message, reaction, mention, etc.)
- Channel filter (specific channels or workspace-wide)
- Watch Whole Workspace setting
When an event arrives:
- Extract the event type and channel from the Slack payload
- Match against all discovered triggers
- Forward to workflows where:
- Event type matches, AND
- Channel matches (or trigger watches whole workspace)
| Slack Event | n8n Trigger Setting |
|---|---|
message |
"New Message Posted to Channel" |
app_mention |
"Bot/App Mention" |
reaction_added |
"Reaction Added" |
file_shared |
"File Shared" |
file_public |
"File Made Public" |
channel_created |
"New Public Channel Created" |
team_join |
"New User Created" |
* |
"Any Event" |
You need to understand this before setting up Jira workflows.
n8n's Jira Trigger node unconditionally calls the Jira REST API to register its own webhooks whenever a workflow is activated (and deregister them on deactivation). This is hardcoded in the node's lifecycle hooks (
JiraTrigger.node.tswebhookMethods.default.checkExists/create/delete) and there is no environment variable or configuration option in n8n to disable it. If these API calls fail, workflow activation fails entirely — n8n does not gracefully degrade.This creates two problems when using Unihook:
- Duplicate webhooks — You register a webhook in Jira pointing at Unihook (
/jira/events), but n8n also registers its own webhooks pointing directly at n8n, bypassing Unihook entirely.- Activation failures — If n8n cannot reach the Jira API (e.g. network restrictions, user permissions), workflows with Jira Trigger nodes cannot be activated at all.
The workaround is to configure the Jira credential in n8n with a domain that does not point to your real Jira instance. Instead, point it at a lightweight service that returns the minimal API responses n8n expects. This satisfies n8n's webhook registration calls without creating real webhooks in Jira or causing activation failures. Unihook handles all actual event routing — the credential is only needed to keep n8n happy.
See Jira Credential Workaround below for the exact setup.
Note: The Slack Trigger node does not have this problem — it does not call out to Slack during workflow activation, so no workaround is needed for Slack.
-
Register a webhook in your Jira instance:
- Go to Settings → System → WebHooks (Jira Server/Data Center) or use the Jira REST API
- Set the URL to:
https://your-domain.com/jira/events - If you use n8n's
authenticateWebhookfeature, append the same query parameter to this URL (see Query Parameter Forwarding) - Select the events you want to forward (or select all)
-
Set up the Jira credential workaround in n8n (see below)
-
Create n8n workflows with Jira Trigger nodes:
- Add a "Jira Trigger" node to your workflow
- Configure the trigger's Events to match the events you want (e.g.
jira:issue_created,comment_created, or*for all) - Attach the workaround Jira credential (not a real one)
- Activate the workflow
-
Unihook routes events to matching workflows based on the
webhookEventfield in the Jira payload.
Because n8n's Jira Trigger node unconditionally registers webhooks via the Jira REST API during workflow activation (see above), you need a service that responds to those API calls. Unihook includes built-in mock endpoints for this purpose — no separate mock server is required.
What n8n calls during the Jira Trigger lifecycle:
| When | Method | Endpoint | Expected Response |
|---|---|---|---|
| Credential validation | GET |
/rest/api/2/myself |
200 with a JSON user object |
| Workflow activation | GET |
/rest/webhooks/1.0/webhook |
200 with [] (empty array) |
| Workflow activation | POST |
/rest/webhooks/1.0/webhook |
201 with a JSON webhook object containing a self URL |
| Workflow deactivation | DELETE |
/rest/webhooks/1.0/webhook/{id} |
204 |
Unihook serves all of these endpoints natively (see src/routes/provider_jira.rs).
Create the Jira credential in n8n with its domain pointing at Unihook:
| Field | Value | Notes |
|---|---|---|
| Type | Jira Software Cloud API |
|
| Domain | http://your-unihook-host:3000 |
Points at Unihook, not real Jira |
[email protected] |
Arbitrary — the mock accepts anything | |
| API Token | noop |
Arbitrary — the mock accepts anything |
Attach this credential to your Jira Trigger nodes. When n8n activates the workflow, its webhook registration calls hit Unihook's mock endpoints and succeed silently. Unihook handles all actual event delivery from Jira.
Unihook discovers workflows with Jira Trigger nodes via the n8n API. For each trigger, it extracts:
- Event types — The list of Jira event types the trigger listens for (e.g.
jira:issue_created,comment_updated,*)
When a Jira webhook event arrives at /jira/events:
- Extract the
webhookEventfield from the payload - Match against all discovered Jira triggers
- Forward to workflows where the event type matches (exact match or wildcard
*)
| Category | Events |
|---|---|
| Issues | jira:issue_created, jira:issue_updated, jira:issue_deleted |
| Comments | comment_created, comment_updated, comment_deleted |
| Boards | board_created, board_updated, board_deleted, board_configuration_changed |
| Sprints | sprint_created, sprint_started, sprint_updated, sprint_closed, sprint_deleted |
| Projects | project_created, project_updated, project_deleted |
| Versions | jira:version_created, jira:version_updated, jira:version_released, jira:version_unreleased, jira:version_moved, jira:version_deleted |
| Users | user_created, user_updated, user_deleted |
| Worklogs | worklog_created, worklog_updated, worklog_deleted |
| Issue Links | issuelink_created, issuelink_deleted |
| Options | option_voting_changed, option_watching_changed, option_unassigned_issues_changed, option_subtasks_changed, option_attachments_changed, option_issuelinks_changed, option_timetracking_changed |
| Wildcard | * (matches all events) |
You need to understand this before setting up GitHub workflows.
Like Jira, n8n's GitHub Trigger node calls the GitHub REST API to register its own webhooks whenever a workflow is activated. This is hardcoded in the node's lifecycle hooks (
GithubTrigger.node.ts). If these API calls fail, workflow activation fails entirely.Additionally, n8n's GitHub Trigger generates a random HMAC secret during webhook registration and verifies every incoming payload against it using the
X-Hub-Signature-256header. This verification cannot be disabled — if the signature is missing or invalid, n8n returns401 Unauthorized.Since the user's GitHub webhook (pointing at Unihook) uses a different secret than the one n8n generated, the original signature from GitHub won't pass n8n's verification. Unihook solves this by re-signing each forwarded payload with n8n's per-workflow secret, which it reads from the workflow's
staticDatavia the n8n API.See ADR-001: GitHub Webhook Payload Re-signing for the full technical rationale.
The workaround for webhook registration is the same pattern as Jira: point the GitHub credential in n8n at a mock service instead of real GitHub.
Note: The Slack Trigger node does not have this problem — it does not call out to Slack during workflow activation, so no workaround is needed for Slack.
-
Create a webhook on your GitHub repository (or organisation):
- Go to Settings → Webhooks → Add webhook
- Set the Payload URL to:
https://your-domain.com/github/events - Set Content type to
application/json - (Recommended) Set a secret — Unihook can verify inbound signatures when
GITHUB_WEBHOOK_SECRETis set (see Inbound Signature Verification) - Select the events you want to forward (or select "Send me everything")
-
Set up the GitHub credential workaround in n8n (see below)
-
Create n8n workflows with GitHub Trigger nodes:
- Add a "GitHub Trigger" node to your workflow
- Configure the Owner and Repository to match the repository the webhook is on
- Configure the trigger's Events to match the events you want (e.g.
push,issues, or*for all) - Attach the workaround GitHub credential (not a real one)
- Activate the workflow
-
Unihook routes events to matching workflows based on the
X-GitHub-Eventheader and the repository owner/name in the payload.
Because n8n's GitHub Trigger node registers webhooks via the GitHub REST API during workflow activation (see above), you need a service that responds to those API calls. Unihook includes built-in mock endpoints for this purpose — no separate mock server is required. As a bonus, Unihook's GitHub mock intercepts the HMAC secret that n8n generates during webhook creation and stores it in its SQLite database, enabling automatic payload re-signing without relying on n8n's staticData.
What n8n calls during the GitHub Trigger lifecycle:
| When | Method | Endpoint | Expected Response |
|---|---|---|---|
| Credential validation | GET |
/user |
200 with a JSON user object |
| Webhook check | GET |
/repos/{owner}/{repo}/hooks |
200 with [] (empty array) |
| Webhook creation | POST |
/repos/{owner}/{repo}/hooks |
201 with a JSON webhook object containing id |
| Webhook deletion | DELETE |
/repos/{owner}/{repo}/hooks/{id} |
204 |
Unihook serves all of these endpoints natively (see src/routes/provider_github.rs). When POST /repos/{owner}/{repo}/hooks is called, Unihook extracts the webhook_id from config.url and the secret from config.secret, storing both in the database for later re-signing.
Create the GitHub credential in n8n with its server pointing at Unihook:
| Field | Value | Notes |
|---|---|---|
| Type | GitHub API |
|
| Server | http://your-unihook-host:3000 |
Points at Unihook, not real GitHub |
| User | noop |
Arbitrary — the mock accepts anything |
| Access Token | noop |
Arbitrary — the mock accepts anything |
Attach this credential to your GitHub Trigger nodes. When n8n activates the workflow, its webhook registration calls hit Unihook's mock endpoints. Unihook captures the HMAC secret and handles all actual event delivery from GitHub, including re-signing payloads for n8n's signature verification.
Unihook discovers workflows with GitHub Trigger nodes via the n8n API. For each trigger, it extracts:
- Event types — The list of GitHub event types the trigger listens for (e.g.
push,issues,*) - Owner — The repository owner (e.g.
n8n-io) - Repository — The repository name (e.g.
n8n) - Webhook secret — The HMAC secret from n8n's
staticData(used for re-signing)
When a GitHub webhook event arrives at /github/events:
- Extract the event type from the
X-GitHub-Eventheader - Extract the owner and repository from the payload
- Match against all discovered GitHub triggers where:
- Event type matches (exact match or wildcard
*), AND - Owner matches (case-insensitive), AND
- Repository matches (case-insensitive)
- Event type matches (exact match or wildcard
- For each matching trigger, re-sign the payload with that workflow's webhook secret and forward
| Category | Events |
|---|---|
| Code | push, create, delete |
| Pull Requests | pull_request, pull_request_review, pull_request_review_comment |
| Issues | issues, issue_comment |
| CI/CD | check_run, check_suite, deployment, deployment_status |
| Releases | release |
| Repository | repository, repository_import, repository_vulnerability_alert, public, fork, star, watch |
| Organisation | organization, org_block, membership, member, team, team_add |
| GitHub Apps | github_app_authorization, installation, installation_repositories |
| Other | commit_comment, deploy_key, gollum, label, marketplace_purchase, milestone, page_build, project, project_card, project_column, security_advisory, status |
| Special | ping (handled automatically — acknowledged but not routed) |
| Wildcard | * (matches all events) |
Unihook supports optional HMAC-SHA256 verification of incoming webhook payloads. When enabled, events that fail verification are rejected with 401 Unauthorized before any routing occurs.
| Service | Env Var | Header Verified | Signing Standard |
|---|---|---|---|
| GitHub | GITHUB_WEBHOOK_SECRET |
X-Hub-Signature-256 |
GitHub webhook security |
How it works: GitHub computes HMAC-SHA256(body, secret) and sends it as sha256=<hex_digest> in the X-Hub-Signature-256 header. Unihook recomputes the HMAC using the configured env var and compares using constant-time equality.
Opt-in: If the env var is not set, verification is skipped entirely and the endpoint accepts any well-formed request (backward-compatible with existing deployments).
Note: Inbound verification is independent of GitHub's outbound re-signing (see ADR-001). The inbound secret is the one you configure on the webhook pointing at Unihook; the outbound secret is the one n8n generates internally.
See ADR-002: Inbound Webhook Signature Verification for the full technical rationale.
n8n's Jira Trigger node has an optional authenticateWebhook parameter that validates incoming requests using an httpQueryAuth credential — a query parameter appended to the webhook URL.
Unihook supports this transparently by forwarding any query parameters from the inbound Jira request URL to the n8n webhook URL. To use it:
- Enable
authenticateWebhookon your Jira Trigger node in n8n and configure thehttpQueryAuthcredential (e.g. name=secret, value=abc123). - When registering your Jira webhook, append the same query parameter to the Unihook URL:
https://your-domain.com/jira/events?secret=abc123 - Jira sends events to the URL with the query parameter. Unihook captures it and appends it to the n8n webhook URL when forwarding, so n8n's credential validation passes.
No additional environment variables are required.
| Endpoint | Method | Description |
|---|---|---|
/slack/events |
POST | Receives Slack events (configure in Slack app) |
/jira/events |
POST | Receives Jira webhook events (configure in Jira) |
/github/events |
POST | Receives GitHub webhook events (configure in GitHub) |
/health |
GET | Health check — reports loaded trigger counts |
server {
listen 443 ssl;
server_name your-domain.com;
# SSL configuration...
location /slack/events {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /jira/events {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /github/events {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}# Full test run (starts Docker, runs tests, stops Docker)
./scripts/run-integration-tests.sh
# Keep Docker running after tests (useful for debugging)
./scripts/run-integration-tests.sh --keep-running
# Skip Docker startup (when containers are already running)
./scripts/run-integration-tests.sh --skip-docker
# Run specific tests
./scripts/run-integration-tests.sh --filter test_jiraThe integration test environment uses Unihook's built-in provider API mock endpoints (the same credential workarounds described above for Jira and GitHub). The test setup script creates "dud" credentials in n8n with their API endpoints pointing at http://n8n-unihook:3000 on the Docker test network. When n8n activates trigger workflows, its webhook registration calls are intercepted by Unihook, which stores any secrets (e.g. GitHub HMAC) in its SQLite database. See docker-compose.test.yml and scripts/run-integration-tests.sh for the implementation.
- Check the health endpoint:
curl http://localhost:3000/health - Verify triggers are loaded: The health response shows
slack_triggers_loaded,jira_triggers_loaded, andgithub_triggers_loadedcounts - Check logs:
docker logs n8n-unihook - Ensure workflows are active in n8n (inactive workflows only receive test webhook events)
- Ensure the service is publicly accessible
- Check that
/slack/eventsreturns 200 for POST requests - The service handles URL verification automatically
- Verify the
webhookEventfield is present in the Jira payload - Check that the workflow's Jira Trigger node is configured for the correct event types
- Use wildcard (
*) events during debugging to match everything
- Verify the
X-GitHub-Eventheader is present (GitHub always sends this) - Check that the workflow's GitHub Trigger node is configured for the correct owner and repository — these must match the repository sending events (case-insensitive)
- Check that the event type matches (e.g. the trigger listens for
pushbut GitHub is sendingissues) - Use wildcard (
*) events during debugging to match all event types for a given repo
- This means the payload re-signing failed or the webhook secret is stale
- Ensure the workflow has been activated at least once (so
staticDatais populated with the webhook secret) - Trigger a refresh by restarting Unihook or waiting for the next refresh interval
- Check logs for
"No webhook secret available"warnings
- Verify
N8N_API_URLis correct and accessible from the container - Check that your
N8N_API_KEYhas read access to workflows
MIT