Skip to content

Consume Embers webhooks in n8n

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.

Updated 3 min read Solo and above

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.

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

  1. 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.

  2. 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.

  3. 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.

  4. 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}.

    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.

  5. 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.

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.

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

Was this article helpful?

Still stuck?

Send us the article you were following and what happened. We answer from the same inbox that writes these pages.

Email support

to move to open esc to close