> ## Documentation Index
> Fetch the complete documentation index at: https://docs.linkiasoft.com/llms.txt
> Use this file to discover all available pages before exploring further.

# n8n: run a workflow when a lead moves

> Receive a signed webhook in n8n, verify it, and act on it — no polling.

When a lead reaches a stage you care about, have n8n do something: post to Slack, write a
row in Sheets, open a task in Jira, message the customer back through us.

We push to n8n. You don't poll, and n8n doesn't need credentials for our API unless the
workflow writes back.

## What you need first

<Steps>
  <Step title="An n8n workflow with a Webhook node">
    Add a **Webhook** node, method `POST`. n8n gives you two URLs — a *test* URL that only
    listens while you have the editor open, and a *production* URL that works once the
    workflow is active. Start with the test URL.
  </Step>

  <Step title="A webhook endpoint in Linkiasoft">
    **Integrations → Webhooks → New endpoint**. Paste the n8n URL, and subscribe to
    `lead.stage.changed`.
  </Step>

  <Step title="The signing secret">
    Shown once when you create the endpoint. Copy it — n8n needs it to verify that a
    request really came from us.
  </Step>
</Steps>

<Note>
  n8n's test URL only accepts **one** request per "Listen for test event" click. If your
  second test seems to vanish, that's why — click listen again.
</Note>

## Pick your fields

A stage-change payload can carry the lead's name, email, phone, value, owner and more.
n8n workflows tend to end up in Slack channels and spreadsheets, so send only what the
workflow uses.

For a Slack notification, this is usually enough:

| Field               | Why                                   |
| ------------------- | ------------------------------------- |
| `lead_id`           | Link back to the lead                 |
| `name`              | Who it is                             |
| `status`            | Where it landed                       |
| `previous_status`   | Where it came from                    |
| `value`, `currency` | The number people actually care about |

Untick `email` and `phone` unless the workflow needs to contact someone. See
[choosing which fields to send](/webhooks#choosing-which-fields-to-send).

## Verify the signature in n8n

Anyone who learns your n8n URL can POST to it. The signature is what makes the difference
between "a lead moved" and "someone told me a lead moved".

Add a **Code** node immediately after the Webhook node:

```javascript theme={null}
const crypto = require('crypto');

const secret = 'whsec_...';               // better: $env.LINKIA_WEBHOOK_SECRET
const header = $input.first().headers['x-linkia-signature'] ?? '';
const rawBody = $input.first().body;

const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));
const timestamp = Number(parts.t);

if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) {
  throw new Error('Signature timestamp missing or too old');
}

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

if (expected !== parts.v1) {
  throw new Error('Bad signature — this did not come from Linkiasoft');
}

return $input.all();
```

<Warning>
  `JSON.stringify(rawBody)` only works because n8n has already parsed the JSON and you are
  re-serialising it. That is fragile — it depends on key order surviving the round trip.

  For anything you rely on, set the Webhook node's **Raw Body** option and hash the raw
  string instead. See [verifying the signature](/webhooks#verifying-the-signature) for why
  this matters.
</Warning>

Put the secret in an n8n environment variable rather than the node body — workflow JSON
gets exported, shared and committed.

## Act on it

After the Code node, a **Switch** or **IF** node on `{{ $json.body.data.status }}` routes
by stage. A Slack node then sends something like:

```
🎉 {{ $json.body.data.name }} moved to {{ $json.body.data.status }}
   ({{ $json.body.data.value }} {{ $json.body.data.currency }})
```

To message the customer back through WhatsApp, add an **HTTP Request** node calling
[`POST /v1/conversations/send`](/api-reference/conversations/send-a-message) with an API key
scoped to `conversations:send`. Read
[the 24-hour rule](/introduction#the-one-whatsapp-rule-that-catches-everyone) first — a
notification your workflow initiates almost always needs an approved template.

## Go to production

Two things change when you switch from the test URL:

1. **Update the endpoint URL in Linkiasoft** to n8n's production URL. They are different
   paths — reusing the test URL means deliveries stop the moment you close the editor.
2. **Activate the workflow.** An inactive workflow returns `404`, which we treat as
   terminal and never retry.

## When nothing arrives

Check in this order — the answer is almost always in step 1.

1. **The delivery log**, on the endpoint's page in Linkiasoft. It shows every attempt, the
   status code, and the exact body we sent. If there's no row at all, the event never fired
   — check you subscribed to the right event, and that the stage actually changed.

2. **The status code in that log.**

   | Code              | Usually means                                              |
   | ----------------- | ---------------------------------------------------------- |
   | `404`             | Workflow isn't active, or you're using the test URL        |
   | `403` / `401`     | n8n has its own auth on the webhook node                   |
   | `500`             | Your Code node threw — most often a failed signature check |
   | *(none)*, timeout | n8n unreachable, or took longer than 10 seconds            |

3. **n8n's own executions list.** If we logged a `2xx` but nothing happened, the request
   arrived and the workflow is at fault.

<Warning>
  Twenty consecutive failures **disables the endpoint** and events stop being queued. If you
  left a broken workflow failing overnight, check whether the endpoint is still enabled
  before debugging anything else.
</Warning>

## Two things that bite

**Both `lead.updated` and `lead.stage.changed` fire on a stage move.** Subscribe to one. If
you take both, every move runs your workflow twice.

**Deliveries are at least once.** A timeout after your workflow already ran means we retry
and it runs again. If it posts to Slack, you get two messages; if it charges something, you
have a real problem. Deduplicate on `{{ $json.body.id }}`, which is stable across every
attempt at the same delivery.
