Intermediate · Lesson 2 of 6 · 15 min
Webhooks and triggers
Understand webhooks versus polling, read a real webhook payload, receive webhooks safely and design triggers that fire on the right events.
Polling versus webhooks
Polling means your automation asks a system on a schedule whether anything is new. It is simple and needs no public URL, but it adds delay and wastes calls when nothing changed. On Zapier, how often a polling trigger checks depends on your plan, with the free plan checking least often.
A webhook reverses the direction: the source system sends an HTTP POST to your URL the moment an event happens. That gives near-instant reactions and no wasted calls, at the cost of running a public endpoint and handling whatever arrives, including duplicates and retries.
Use webhooks when speed matters, such as replying to a new lead within minutes. Polling is fine for reports and syncs where a delay does not hurt.
Anatomy of a payload
A webhook payload is the JSON body describing the event. HighLevel's webhook guide shows the common shape: an event type, a timestamp, a unique webhookId and a data object with the record's fields. Signature headers travel alongside the body.
The event type tells you what happened. The webhookId identifies this delivery so you can recognise it if it arrives again. The data is a snapshot taken when the event fired, which may already be out of date by the time you process it.
{
"type": "ContactCreate",
"timestamp": "2026-10-26T09:14:00.000Z",
"webhookId": "a1b2c3d4-test",
"data": {
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@example.com"
}
}Receiving webhooks safely
Anyone who finds your URL can POST to it, so verify the signature before trusting the body. HighLevel signs deliveries with an Ed25519 key in the X-GHL-Signature header and publishes the public key in its docs. The older X-WH-Signature header was scheduled for deprecation on 1 September 2026, so new integrations should verify X-GHL-Signature.
Acknowledge quickly with a 2xx and do the slow work afterwards. HighLevel treats any non-2xx response, timeout or connection failure as a failure and retries with exponential backoff and random jitter. A slow handler therefore causes repeat deliveries of the same event.
That is why you store each webhookId and skip ones you have already processed. In production, keep processed IDs in a database table with a unique constraint rather than in memory.
Verify the signature against the exact bytes you received, before parsing. If you parse the JSON and re-serialise it, key order or whitespace can change, and a genuine delivery fails the check (or you end up trusting bytes the sender never signed). That is why the sample reads this route's body raw.
import crypto from "node:crypto";
import express from "express";
const app = express();
const ghlPublicKey = process.env.GHL_WEBHOOK_PUBLIC_KEY; // Ed25519 PEM from HighLevel's webhook guide
const processed = new Set(); // use a table with a unique webhook_id column in production
// Raw body on this route: the signature covers the bytes as sent, not a re-serialised copy.
app.post("/webhooks/highlevel", express.raw({ type: "application/json" }), async (req, res) => {
const signature = req.get("x-ghl-signature");
const valid = Boolean(signature) &&
crypto.verify(null, req.body, ghlPublicKey, Buffer.from(signature, "base64"));
if (!valid) return res.status(401).json({ error: "invalid signature" });
const event = JSON.parse(req.body.toString("utf8"));
if (processed.has(event.webhookId)) return res.status(200).json({ status: "duplicate" });
processed.add(event.webhookId);
res.status(200).json({ status: "accepted" }); // acknowledge first
await enqueueForProcessing(event); // your queue or background job
});Designing triggers
Subscribe to the narrowest event that means what you care about. If you only act when a lead is tagged hot, a tag-change event plus an early filter is better than reacting to every contact update and discarding most of them.
Filter as early as possible, and treat the payload as a notification rather than the truth. Before you take an important action, fetch the current record from the API, because events can arrive late or out of order.
Test against a test URL before going live. n8n's Webhook node has separate test and production URLs: the test URL listens while you click Listen for Test Event and shows the data in the editor, while the production URL is registered when you publish the workflow and its runs appear in Executions instead.
Key points
- ◆Polling is simple but delayed; webhooks are instant but need a public endpoint.
- ◆A payload has an event type, a delivery ID and a snapshot of data that may be stale.
- ◆Verify signatures before trusting a webhook body.
- ◆Return 2xx fast and process later, because slow or failed responses trigger retries.
- ◆Deduplicate on the delivery ID, filter early, and re-fetch current state before important actions.
Terms in this lesson
Try it · exercise
Catch a webhook in n8n, inspect the payload and route it with an early filter.
- In an n8n Cloud trial or a self-hosted instance, create a workflow with a Webhook node set to POST and copy its test URL.
- Click Listen for Test Event, then send the sample payload from this lesson with curl -X POST, a content-type: application/json header and the JSON as the body.
- Add an If node after the webhook that checks whether body.data.email is not empty.
- Listen again and send a second request with the email field removed.
You're done when: The Webhook node's output shows type ContactCreate and the webhookId from your request, and the second request with no email goes down the If node's false branch.
