n8n Integration (Self-Hosted)

Connect WarmySender to n8n, the open-source workflow automation tool, to fan out events (email replies, LinkedIn replies, invite acceptances, bounces) into any downstream system — CRM, databases, Slack, Teams, Discord, Sheets, custom APIs.

Why n8n:

Setup:

Step 1: Create a Webhook trigger in n8n
• Open your n8n instance and create a new workflow.
• Add a 'Webhook' node as the trigger (under Core Nodes).
• Set HTTP Method to POST.
• Set Response Mode to 'Immediately' (we need a 200 within 5 seconds — our timeout).
• Activate the workflow and copy the Production URL (NOT the Test URL — test URLs expire).

Step 2: Register in WarmySender
• Settings → Webhooks → New webhook.
• Paste the n8n Production URL.
• Subscribe to desired events (see list below).
• Save and click the test button to confirm delivery.

Step 3: Route by event type
Add a Switch node after the Webhook trigger, routing on {{ $json.type }}:
• reply.received — Email campaign reply
• linkedin.reply_received — LinkedIn message / InMail reply (includes reply body in messageText)
• linkedin.invite_accepted — LinkedIn connection request accepted
• email.bounced — Hard bounce
• email.unsubscribed — Unsubscribe link clicked
• email.opened / email.clicked — Open / link click (high volume)
• prospect.suppressed — Prospect added to suppression list
• limit.hit — Daily sending limit reached
• webhook.test — Test event fired from Settings

Step 4: Add downstream actions
Example chains (from real customer setups):

1) LinkedIn reply → Slack + HubSpot task:
Webhook → IF (type = linkedin.reply_received) → Slack node (post to #sales-replies with {{ $json.data.messageText }}) → HubSpot node (Create Task: 'Follow up on LinkedIn reply').

2) Every reply → Google Sheets log:
Webhook → IF (type contains 'reply') → Google Sheets 'Append Row' (Timestamp, Channel, Prospect, Campaign, Body).

3) Invite accepted → Pipedrive deal:
Webhook → IF (type = linkedin.invite_accepted) → Pipedrive node (Create Deal in 'Connected' stage, mapped from {{ $json.data.prospectLinkedinUrl }}).

4) Per-campaign routing to different Slack channels:
Webhook → Switch (by {{ $json.data.campaignId }}) → multiple Slack outputs, one per channel.

5) Bounce log + ops alert:
Webhook → IF (type = email.bounced) → Postgres Insert → Microsoft Teams node.

Signature Verification in n8n (publicly-reachable endpoints):
Every payload is signed with HMAC-SHA256. The X-Warmy-Signature header is Stripe-style `t=<unix_ms>,v1=<hmac_hex>` — you must parse it and verify against the RAW request body (not the parsed JSON).

Two setup steps:

  1. On the Webhook trigger node, under 'Options', enable 'Raw Body' so the raw bytes are preserved on `$binary.data`.
  2. Add a Function node before the Switch with this code:

const crypto = require('crypto');
const secret = $env.WARMY_WEBHOOK_SECRET; // set in n8n credentials/env — never hardcode
const headers = $input.first().headers;
const sigHeader = headers['x-warmy-signature'] || '';
const parts = Object.fromEntries(sigHeader.split(',').map(p => p.split('=')));
const ts = parts.t; const sig = parts.v1;
if (!ts || !sig) throw new Error('Missing signature');
if (Math.abs(Date.now() - parseInt(ts, 10)) > 5 * 60 * 1000) throw new Error('Stale timestamp');
const rawBody = Buffer.from($input.first().binary.data.data, 'base64').toString('utf8');
const expected = crypto.createHmac('sha256', secret).update(ts + '.' + rawBody).digest('hex');
const a = Buffer.from(sig, 'hex'); const b = Buffer.from(expected, 'hex');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) throw new Error('Bad signature');
// re-parse body for downstream nodes
$input.first().json = JSON.parse(rawBody);
return $input.all();

If Raw Body isn't enabled, re-serializing $json will NOT byte-match what we signed (key order / whitespace / unicode escaping all differ) and verification will fail 100% of the time.

LinkedIn Event Payload Shape:
linkedin.reply_received:
data: { prospectId, prospectName, prospectLinkedinUrl, campaignId, enrollmentId, linkedinAccountId, messageText, threadId, receivedAt }
linkedin.invite_accepted:
data: { prospectId, prospectName, prospectLinkedinUrl, campaignId, enrollmentId, linkedinAccountId, acceptedAt }

Note: linkedin.reply_received fires once per enrollment on the FIRST reply. Subsequent messages in the same thread don't re-fire. If you need every inbound message, contact support — this isn't currently exposed as a standalone event.

Advanced: WarmySender API from n8n:
Beyond webhook consumption, use n8n HTTP Request nodes to call the WarmySender REST API:
• Create prospects: POST /api/v1/prospects (email, firstName, lastName, company, linkedinUrl, custom fields).
• Enroll in campaign: POST /api/v1/campaigns/:id/enrollments with prospectIds.
• Unenroll: DELETE /api/v1/campaigns/:id/enrollments with emails or prospectIds.
• Example: lead replies on WhatsApp via Interakt → n8n workflow unenrolls from WarmySender email campaign to prevent duplicate outreach.

Account Safety (LinkedIn Events):
WarmySender's webhook delivery does not make any LinkedIn API calls on your behalf — payloads come from our database state only. Toggling webhooks on or off doesn't change how often we poll LinkedIn via Unipile; your LinkedIn account's daily action limits are driven by campaign sends / invites / profile views, not by webhook activity. Enable as many webhooks as you need.

Reliability:

Best Practices:

Zapier / Make:
Same pattern — 'Webhooks by Zapier' (Catch Hook) or Make's Webhook trigger. Copy the URL, paste into WarmySender Settings → Webhooks, chain downstream modules. Zapier/Make handle format translation between our JSON and Slack/HubSpot/etc., but for native Slack, see the 'Slack Notifications' guide — our direct Slack auto-format is simpler than going through Zapier.

Related guides in Integrations

Back to all documentation | Contact support