# Consume Embers webhooks in n8n

URL: https://useembers.com/help/integrations/n8n/
Category: Integrations
Plan: Solo and above
Updated: 2026-09-12
Last verified: 2026-09-12

> A community recipe: point a signed Embers endpoint at an n8n Webhook node, verify the HMAC in a function node, then run your own workflow.

n8n can receive Embers leads, but nothing about it is an Embers feature. This is a community recipe: a generic n8n Webhook node listening to the same signed HTTPS webhook that Zapier, Make, Clay, and hand-written receivers use. Embers does not build it, test it, or support it.

> **Warning: Community recipe, not maintained by Embers**
>
> n8n does not appear on the Integrations screen in the Embers app. Zapier, Make, and Clay are listed there as recipes on the HTTPS destination; n8n is not listed at all. There is no n8n node published by Embers and no n8n-specific behaviour in the product. If your workflow breaks, the delivery log on the Webhooks screen will tell you what Embers sent and what your endpoint answered, and everything after that is yours to debug.

> **Warning: Solo and above, administrators only**
>
> The Webhooks screen requires Solo or above, and only an account administrator can reach it.

## Before you start

- An n8n instance, cloud or self-hosted, reachable over public HTTPS on port 443.
- The production webhook URL from your n8n Webhook node, not the test URL, if you want deliveries to keep working after the editor closes.
- The `whsec_` signing secret from the Embers endpoint.

## Set up the workflow

### Add a Webhook node

Create a workflow with a Webhook node set to accept `POST`. Take the HTTPS URL it gives you.

Configure the node so your function can read the raw request body. The signature is computed over the exact bytes Embers sent, so a body that has been parsed and re-serialised will not match.

### Register the URL in Embers

In the Embers app, open **Developer**, then **Webhooks** (`/developer/webhooks`) and add the n8n URL as an endpoint. Public HTTPS on port 443 only, and a URL with a `#` fragment is rejected because browsers never send fragments in an HTTP request. Redirects are not followed.

Copy the signing secret shown when the endpoint is created and store it as an n8n credential or environment variable.

### Subscribe to the events you want

Subscribe to `lead.created` for one event per newly qualified lead. `lead.engagement.added` is the highest-volume event and is off by default on new endpoints. `lead.status_changed` and `report.generated` are there when you need them.

### Verify the signature in a function node

Put a function node immediately after the Webhook node and reject anything that fails. Embers sends Standard Webhooks headers: `webhook-id`, `webhook-timestamp` (Unix seconds), and `webhook-signature` in the form `v1,<base64 HMAC-SHA256>`. The signature covers `{webhook-id}.{webhook-timestamp}.{raw body}`.

```js
const crypto = require('crypto');

const headers = $input.first().json.headers;
const rawBody = $input.first().json.body; // the exact string Embers sent
const secret = Buffer.from($env.EMBERS_WEBHOOK_SECRET.replace('whsec_', ''), 'base64');

const id = headers['webhook-id'];
const timestamp = headers['webhook-timestamp'];
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!id || !timestamp || age > 300) {
  throw new Error('Missing headers or stale timestamp');
}

const expected = crypto
  .createHmac('sha256', secret)
  .update(`${id}.${timestamp}.${rawBody}`)
  .digest('base64');

const ok = String(headers['webhook-signature'] || '')
  .split(' ')
  .some((part) => {
    const value = part.split(',')[1];
    if (!value || value.length !== expected.length) return false;
    return crypto.timingSafeEqual(Buffer.from(value), Buffer.from(expected));
  });

if (!ok) {
  throw new Error('Bad signature');
}

return [{ json: JSON.parse(rawBody) }];
```

Compare in constant time, and treat the `webhook-signature` header as a space separated list so a secret rotation with two valid signatures does not lock you out.

### Deduplicate, then do the work

Delivery is at least once and retries can arrive out of order. Store `webhook-id` and drop anything you have already processed. Join people on `data.lead.linkedin_urn`, falling back to `data.lead.id` when the URN is blank on an older record.

Answer with a `2xx` quickly. Any `2xx` is accepted, so acknowledge first and run the slow branches after.

## What Embers sends

The payload is identical to every other receiver. A lead is delivered when its score is above zero, it matched your ICP or you approved it manually, and its company is not blocklisted. `data.lead.contact` is present on Solo and above and carries only stored email and phone, never a fresh lookup. The full field list is in the [webhook payload reference](/help/integrations/webhook-payload-reference/).

## Troubleshooting

**Every request fails the signature check.** Almost always the body. n8n must hand your function the raw string, not a re-encoded object. Compare the length of your computed value against the header before you look anywhere else.

**Deliveries stopped after a few days.** Embers retries network errors, `408`, `425`, `429`, and `5xx` seven times, with the last attempt about 6.3 days after the first. Other `4xx` responses are terminal. Either way the endpoint pauses. See [Webhook endpoint is paused](/help/troubleshooting/webhook-endpoint-paused/).

**Test URL, not production URL.** An n8n test URL only listens while the editor is open. Register the production URL.

## Related

- [Signed webhooks](/help/integrations/webhooks/): Endpoints, events, retries, and the delivery log.

- [Verify webhook signatures](/help/integrations/verify-webhook-signatures/): The signature scheme in full.

- [Webhook payload reference](/help/integrations/webhook-payload-reference/): Every field in every event.
