<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Robbie Cahill</title>
    <description>The latest articles on DEV Community by Robbie Cahill (@robbiecahill).</description>
    <link>https://dev.to/robbiecahill</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F516032%2F8db23a5a-729b-4064-9d73-034cde38e34b.jpeg</url>
      <title>DEV Community: Robbie Cahill</title>
      <link>https://dev.to/robbiecahill</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/robbiecahill"/>
    <language>en</language>
    <item>
      <title>I Built a Localhost Tunneling tool in TypeScript - Here's What Surprised Me</title>
      <dc:creator>Robbie Cahill</dc:creator>
      <pubDate>Tue, 20 Jan 2026 09:43:15 +0000</pubDate>
      <link>https://dev.to/robbiecahill/i-built-a-localhost-tunneling-tool-in-typescript-heres-what-surprised-me-17eg</link>
      <guid>https://dev.to/robbiecahill/i-built-a-localhost-tunneling-tool-in-typescript-heres-what-surprised-me-17eg</guid>
      <description>&lt;p&gt;For years, I relied on indispensable but proprietary tunneling tools like &lt;code&gt;ngrok&lt;/code&gt;. They are fantastic for exposing local development servers to the public internet, making webhook development and cross-device testing a breeze. Yet, the software engineer in me was always curious about the "black box." How did it really work? Could I build an open-source version that was just as simple and effective?&lt;/p&gt;

&lt;p&gt;This curiosity led me to create &lt;a href="https://tunnelmole.com?utm_source=iBuiltALocalhostTunnelArticle" rel="noopener noreferrer"&gt;Tunnelmole&lt;/a&gt;, a localhost tunneling tool written entirely in TypeScript both on the client and server side. The journey was more challenging and surprising than I anticipated. I dove in expecting to wrestle with network protocols and asynchronous code, which I did. But I also found myself grappling with unexpected challenges, from fighting off phishing scammers to hitting the limitations of modern JavaScript APIs.&lt;/p&gt;

&lt;p&gt;This article shares the four most surprising lessons I learned while building a localhost tunnel from scratch. It's a story about technical discovery, unforeseen consequences, and the trade-offs between high-level abstractions and low-level control.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Dark Side: Phishing Scammers Love Tunneling Tools
&lt;/h2&gt;

&lt;p&gt;One of the first "success" metrics for Tunnelmole was, unfortunately, its adoption by malicious actors. Shortly after launching, I noticed a surge in usage that correlated with a spike in abuse reports. Scammers were using Tunnelmole to host phishing sites. They'd set up a fraudulent login page on their local machine, use Tunnelmole to get a public, HTTPS-enabled URL for it, and then use that URL in phishing campaigns.&lt;/p&gt;

&lt;p&gt;This presented a significant problem. The service was designed for developers, but its core value proposition a free, anonymous public URL was a magnet for abuse. From the scammer's perspective, it was perfect: they could hide their server's true IP address behind Tunnelmole's infrastructure, making them harder to track down. When their fraudulent &lt;code&gt;tunnelmole.net&lt;/code&gt; URL was reported, the abuse complaint would come to my hosting provider, not theirs.&lt;/p&gt;

&lt;p&gt;Initially, I played a game of whack-a-mole, manually taking down abusive tunnels as reports came in. But that was never going to be a sustainable approach in the long run. I needed a systemic solution that would make Tunnelmole an unattractive platform for phishers without alienating legitimate developer users.&lt;/p&gt;

&lt;p&gt;The solution came in two parts, focused on one goal: deanonymizing the origin of the tunnel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution 1: The &lt;code&gt;X-Forwarded-For&lt;/code&gt; Header
&lt;/h3&gt;

&lt;p&gt;The first change was to ensure the &lt;code&gt;X-Forwarded-For&lt;/code&gt; HTTP header was always present in requests passing through the tunnel. This standard header is used by proxies and load balancers to indicate the IP address of the client that initiated the request.&lt;/p&gt;

&lt;p&gt;When a request comes into the Tunnelmole service, the service now adds this header, setting its value to the IP address of the &lt;code&gt;tunnelmole&lt;/code&gt; client user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X-Forwarded-For: &amp;lt;IP address of the Tunnelmole client user&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means that investigators can easily check this header and find the IP of the server hosting the malicious content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution 2: IP in the URL
&lt;/h3&gt;

&lt;p&gt;The second, more visible change was to embed the client's IP address directly into the randomly generated tunnel URLs. A typical Tunnelmole URL now looks like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://xj38d-ip-111-111-111-111.tunnelmole.net&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This makes it abundantly clear, even to a non-technical person, where the content is ultimately being served from. When an abuse report comes in, in addition to taking down the tunnel, I can reply with a simple, clear explanation: "The content is not hosted on our servers. As you can see from the URL, it originates from the IP address &lt;code&gt;111.111.111.111&lt;/code&gt;. Please direct your complaint to the hosting provider for that IP."&lt;/p&gt;

&lt;p&gt;These two changes were game-changers. The abuse reports plummeted almost overnight. The scammers realized that Tunnelmole no longer offered them the anonymity they craved. If their origin IP was going to be exposed anyway, they might as well host the phishing site directly on their own server.&lt;/p&gt;

&lt;p&gt;This experience also underscored the importance of domain separation. The main website is &lt;code&gt;tunnelmole.com&lt;/code&gt;, while the tunnels themselves operate on &lt;code&gt;tunnelmole.net&lt;/code&gt;. This was a deliberate choice to protect the reputation and SEO of the main domain. If malicious user generated content hosted on a &lt;code&gt;.tunnelmole.net&lt;/code&gt; subdomain caused the entire domain to be blacklisted, it wouldn't take the &lt;code&gt;tunnelmole.com&lt;/code&gt; website down with it.&lt;/p&gt;

&lt;p&gt;Having malicious content hosted on the main domain, even if I didn't put it there myself, would have serious SEO consequences. Google massively downranks any domains known to have ever had malicious content.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Abstraction Disappointment: &lt;code&gt;fetch&lt;/code&gt; Does Not Give You the Pure HTTP Request
&lt;/h2&gt;

&lt;p&gt;When it came time to write the client-side code that receives a request from the tunnel and forwards it to the user's local server, my first instinct was to reach for a modern, high-level API like &lt;code&gt;fetch&lt;/code&gt;. It's the standard for making HTTP requests in browsers and Node.js. This is what i'd used for much of my life for interacting with APIs. My thinking was simple: take the incoming data from the WebSocket, construct a &lt;code&gt;fetch&lt;/code&gt; request, and send it to &lt;code&gt;localhost&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I quickly ran into a wall. High-level abstractions like &lt;code&gt;fetch&lt;/code&gt; and even &lt;code&gt;axios&lt;/code&gt; are designed for convenience, not for perfect, byte-for-byte proxying. They are "opinionated" and manipulate the underlying HTTP request in ways that are helpful for most application development but disastrous for a tunneling tool.&lt;/p&gt;

&lt;p&gt;Here were the main problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Header Manipulation:&lt;/strong&gt; &lt;code&gt;fetch&lt;/code&gt; automatically lowercases all header names. This is usually fine, as HTTP header names are case-insensitive. However, a true tunnel should be transparent. It shouldn't alter the data passing through it. If a developer is debugging a case-sensitive header issue with a poorly-behaved client, the tunnel shouldn't hide the problem.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Body Parsing:&lt;/strong&gt; &lt;code&gt;fetch&lt;/code&gt; wants to be helpful with the request and response bodies. It tries to parse them, stream them, and handle content encoding. But for a tunnel, the body is just an opaque bag of bytes. It could be a JSON payload, a multipart form upload, or something binary. I needed to grab the raw body as a &lt;code&gt;Buffer&lt;/code&gt; and forward it verbatim. Trying to force &lt;code&gt;fetch&lt;/code&gt; to "un-process" the body was clumsy and unreliable.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Lack of Low-Level Control:&lt;/strong&gt; I couldn't get the raw, untouched HTTP request. The tool was always one step removed from the underlying socket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After struggling with these libraries, I realized I was using the wrong tool for the job. I didn't need a convenient API for &lt;em&gt;making&lt;/em&gt; requests; I needed a low-level tool for &lt;em&gt;reconstructing&lt;/em&gt; them.&lt;/p&gt;

&lt;p&gt;The solution was to go back to basics and use Node.js's built-in &lt;code&gt;http&lt;/code&gt; module.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;http.request()&lt;/code&gt; method provides the granular control I needed. It allows you to set headers exactly as you receive them, write the request body directly from a &lt;code&gt;Buffer&lt;/code&gt;, and manage the connection at a much lower level.&lt;/p&gt;

&lt;p&gt;By working with the &lt;code&gt;http&lt;/code&gt; module, requests could be treated as generic &lt;code&gt;Buffer&lt;/code&gt; objects. This ensured that any type of data JSON, HTML, images, binary files could be proxied faithfully without being accidentally misinterpreted or modified by an overly helpful abstraction layer. The tunnel could finally be the transparent conduit it was meant to be.&lt;/p&gt;

&lt;p&gt;The only downside here is it does not come with a nice &lt;code&gt;async/await&lt;/code&gt; Promise based workflow, so I had to go back to using callbacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. You Can and Should Send Typed JSON as WebSocket Messages
&lt;/h2&gt;

&lt;p&gt;A tunnel works by establishing a persistent connection between the client (&lt;code&gt;tmole&lt;/code&gt;) and the server (&lt;code&gt;tunnelmole.net&lt;/code&gt;). I chose WebSockets for this, as they provide a full-duplex communication channel over a single TCP connection, perfect for this kind of proxying.&lt;/p&gt;

&lt;p&gt;The fundamental challenge with WebSockets is that they just transmit messages, either as strings or binary data. You are responsible for defining the structure and meaning of those messages. The naive approach would be to just send raw data and use a series of &lt;code&gt;if/then/else&lt;/code&gt; statements or complex prefixes to figure out what each message represents. Is this the start of a connection? Is it an HTTP request from the server? An HTTP response from the client? This path leads to brittle, unmaintainable spaghetti code.&lt;/p&gt;

&lt;p&gt;Instead, I decided to build a simple, explicit messaging "framework" on top of the WebSocket connection. Every message is a JSON object with a &lt;code&gt;type&lt;/code&gt; property. This property dictates the shape of the message's payload and determines which handler function should process it.&lt;/p&gt;

&lt;p&gt;On the server, when a message arrives from a client, a simple router looks at the &lt;code&gt;type&lt;/code&gt; and dispatches it to the correct handler.&lt;/p&gt;

&lt;p&gt;Here is a simplified look at the server-side dispatcher:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified message dispatcher on the server&lt;/span&gt;
&lt;span class="nx"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// A map of message types to handler functions&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;messageHandlers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`No handler for message type: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to parse or handle message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach turns a chaotic stream of data into a structured, event-driven system. We have a dedicated handler for each type of message. For example, when a client first connects, it sends an &lt;code&gt;initialize&lt;/code&gt; message, which is processed by the &lt;code&gt;initializeHandler&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The real power of this pattern becomes clear when handling the core tunneling logic. When a public request hits a user's URL (e.g., &lt;code&gt;https://...tunnelmole.net/api/test&lt;/code&gt;), the server packages it into a &lt;code&gt;ForwardedRequestMessage&lt;/code&gt; and sends it to the client over the WebSocket.&lt;/p&gt;

&lt;p&gt;The client receives this message and its &lt;code&gt;forwardedRequest&lt;/code&gt; handler fires. This handler is the bridge between the WebSocket world and the &lt;code&gt;localhost&lt;/code&gt; world.&lt;/p&gt;

&lt;p&gt;Here's a closer look at the client-side handler, which uses the &lt;code&gt;http&lt;/code&gt; module we discussed earlier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// From: src/message-handlers/forwarded-request.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ForwardedRequestMessage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../messages&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Options&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../options&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HostipWebSocket&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../websocket-wrapper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;forwardedRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;forwardedRequestMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ForwardedRequestMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="nx"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HostipWebSocket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Options&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;forwardedRequestMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. Configure the local HTTP request options&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RequestOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localhost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// The user's local port (e.g., 3000)&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;headers&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Create and dispatch the request to the local server&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;responseBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 3. Collect response data chunks as they stream in&lt;/span&gt;
        &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;responseBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;responseBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="c1"&gt;// 4. When the local response ends, send it back to the server&lt;/span&gt;
        &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;end&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;forwarded-response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="c1"&gt;// The body is Base64 encoded to safely transmit binary data in JSON&lt;/span&gt;
                &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;responseBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Error forwarding request to localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Inform the server that the request failed&lt;/span&gt;
        &lt;span class="nx"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;forwarded-response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Bad Gateway&lt;/span&gt;
            &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/plain&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Tunnelmole: Error connecting to localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// 5. Write the request body if it exists&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// The body from the server is Base64 encoded&lt;/span&gt;
        &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This typed, message-driven architecture is clean, self-documenting, and extensible. Adding a new capability to the tunnel is as simple as defining a new message type and writing a handler for it. It completely avoids the ambiguity of an unstructured data stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Node.js Will Not Hold Your Hand: Memory Leaks Are a Thing
&lt;/h2&gt;

&lt;p&gt;Coming from a PHP background, I was accustomed to a stateless, "share-nothing" architecture. In PHP, every web request starts with a clean slate. Memory and resources are allocated, the script runs, a response is sent, and then everything is torn down. It's exceptionally difficult to create a memory leak that persists between requests unless you go out of your way to remove built in safety limits and misconfigure things.&lt;/p&gt;

&lt;p&gt;Node.js is a different beast entirely. It's a stateful, long-running process. This is one of its greatest strengths it's fast and efficient because it doesn't have the overhead of bootstrapping and tearing down on every request. But this power comes with responsibility. Any object you create can potentially live for the entire lifetime of the process. If you forget to clean up, you will have a memory leak.&lt;/p&gt;

&lt;p&gt;I learned this the hard way. The Tunnelmole service needs to keep track of every active client connection. I created a simple &lt;code&gt;Proxy&lt;/code&gt; class to manage this. Here is an oversimplified version of the initial implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Oversimplified initial version of the connection manager&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Proxy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Proxy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// An array to hold all active WebSocket connections&lt;/span&gt;
    &lt;span class="nl"&gt;connections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Connection&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;addConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HostipWebSocket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* ...other params */&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="c1"&gt;// ...other properties&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; 

    &lt;span class="c1"&gt;// ... other methods like findConnectionByHostname() ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code works perfectly... for a while. It adds new connections to the &lt;code&gt;connections&lt;/code&gt; array as clients connect. But what happens when a client disconnects? Nothing. The &lt;code&gt;Connection&lt;/code&gt; object, including its now-defunct WebSocket object, remains in the &lt;code&gt;connections&lt;/code&gt; array forever.&lt;/p&gt;

&lt;p&gt;The array just grew and grew. With each new connection, the server's memory usage crept up. Eventually, the process would exhaust all available memory and crash. I had created a classic memory leak.&lt;/p&gt;

&lt;p&gt;The fix, in hindsight, was obvious. I needed to hook into the WebSocket's &lt;code&gt;close&lt;/code&gt; event and explicitly clean up the stale connection object.&lt;/p&gt;

&lt;p&gt;First, I added a &lt;code&gt;deleteConnection&lt;/code&gt; method to the &lt;code&gt;Proxy&lt;/code&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In the Proxy class&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;deleteConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, I attached a listener to the &lt;code&gt;close&lt;/code&gt; event for every new WebSocket connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// When a new WebSocket connection is established...&lt;/span&gt;
&lt;span class="nx"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Ensure the connection is fully terminated&lt;/span&gt;
    &lt;span class="nx"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;terminate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Use the proxy to remove the connection from the active list&lt;/span&gt;
    &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tunnelmoleClientId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Connection &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tunnelmoleClientId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; closed.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this change in place, the memory leak vanished. The server's memory usage became stable, rising and falling naturally with the number of active users. This experience was a stark reminder that in a long-running environment like Node.js, you are the custodian of memory. You must be diligent about resource cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building Tunnelmole from the ground up was an incredible learning experience that went far beyond writing network code. It forced me to confront the real-world operational challenges of running a public service, appreciate the trade-offs between different levels of API abstraction, and internalize the discipline required for state management in a long-running process.&lt;/p&gt;

&lt;p&gt;The key takeaways were:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Security Through Transparency:&lt;/strong&gt; When building public tools, preventing abuse is as important as the core functionality. Sometimes, the best security measure is to remove the veil of anonymity that attracts bad actors.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Use the Right Level of Abstraction:&lt;/strong&gt; High-level APIs like &lt;code&gt;fetch&lt;/code&gt; are powerful but have their limits. For tasks that require absolute control, like proxying, don't be afraid to drop down to lower-level APIs like Node's &lt;code&gt;http&lt;/code&gt; module.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Structure Your Data Streams:&lt;/strong&gt; Don't treat WebSockets as an unstructured pipe. A simple, typed messaging protocol brings order to chaos and makes your application robust and extensible.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Manage Your State Diligently:&lt;/strong&gt; In stateful environments like Node.js, you are responsible for memory management. Always have a plan for cleaning up objects and resources that are no longer needed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I set out to build an open-source alternative to proprietary software and in the process, I learned quite a lot about NodeJS and the core of the HTTP protocol. If you're a developer who loves to peek inside the black box, I can't recommend a project like this enough.&lt;/p&gt;

&lt;p&gt;If you're interested in checking out the result of this journey, you can &lt;a href="https://tunnelmole.com?utm_source=iBuiltALocalhostTunnelArticle" rel="noopener noreferrer"&gt;try Tunnelmole now&lt;/a&gt; or dive into the full, non oversimplfied code on &lt;a href="https://github.com/robbie-cahill/tunnelmole-client" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. It’s open source, and contributions are always welcome.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>websockets</category>
      <category>opensource</category>
    </item>
    <item>
      <title>AI Agents Deleting Home Folders? Run Your Agent in Firejail and Stay Safe</title>
      <dc:creator>Robbie Cahill</dc:creator>
      <pubDate>Mon, 15 Dec 2025 09:58:37 +0000</pubDate>
      <link>https://dev.to/robbiecahill/ai-agents-deleting-home-folders-run-your-agent-in-firejail-and-stay-safe-409d</link>
      <guid>https://dev.to/robbiecahill/ai-agents-deleting-home-folders-run-your-agent-in-firejail-and-stay-safe-409d</guid>
      <description>&lt;h2&gt;
  
  
  Introduction: The Double-Edged Sword of AI Agents
&lt;/h2&gt;

&lt;p&gt;As a developer, I'm always looking for tools that boost productivity. While building &lt;a href="https://tunnelmole.com/" rel="noopener noreferrer"&gt;Tunnelmole&lt;/a&gt;, an open-source tunneling tool and a popular alternative to ngrok, I've increasingly used AI agents for various coding and business-related tasks. When used correctly and with human oversight, these agents are incredibly powerful. However, giving them unrestricted access to your development machine is like handing over your password to an unpredictable intern - powerful, but potentially catastrophic. If something disastrous happens, in the same way you can't blame an unpredictable over eager intern who was given too much access, you can't really blame the AI agent. Its on you to set up a secure environment for the agent to run in. &lt;/p&gt;

&lt;p&gt;The convenience of letting an agent read files, run commands, and write code directly on your machine is undeniable. But this convenience comes with a massive, often overlooked, risk. What happens when the AI misunderstands a prompt? Or worse, what if a bug in the agent's logic causes it to execute a destructive command?&lt;/p&gt;

&lt;p&gt;This isn't a theoretical problem. A recent post on Reddit sent a chill down the spine of every developer using AI tools. A user on reddit reported that the Claude CLI &lt;a href="https://www.reddit.com/r/ClaudeAI/comments/1pgxckk/claude_cli_deleted_my_entire_home_directory_wiped/" rel="noopener noreferrer"&gt;deleted their entire home directory&lt;/a&gt;, effectively wiping their Mac.&lt;/p&gt;

&lt;p&gt;The culprit was a single, terrifying command generated by the AI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; tests/ patches/ plan/ ~/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most of that command is harmless, targeting project-specific directories. The disaster lies in the final two characters: &lt;code&gt;~/&lt;/code&gt;. In Unix-like systems, &lt;code&gt;~&lt;/code&gt; is a shortcut for the current user's home directory. The command, therefore, translates to "forcefully and recursively delete the 'tests' directory, the 'patches' directory, the 'plan' directory, AND &lt;strong&gt;MY ENTIRE HOME FOLDER&lt;/strong&gt;." Everything: documents, photos, dotfiles, and years of work, all gone in an instant.&lt;/p&gt;

&lt;p&gt;This incident is a stark warning. We must build guardrails. The solution is sandboxing: running the AI agent in a restricted, controlled environment where its capabilities are strictly limited.&lt;/p&gt;

&lt;p&gt;In this guide, I'll show you how to use a powerful tool called &lt;strong&gt;Firejail&lt;/strong&gt; to create a secure sandbox for your VS Code-based AI agent. I personally use Kilo Code, but these instructions are directly applicable to other popular agents like GitHub Copilot and can be easily adapted for agentic IDEs such as Windsurf, or any other tool that runs within VS Code.&lt;/p&gt;

&lt;p&gt;Firejail uses Linux kernel features such as namespaces to work, so this guide will only work on Linux, either running on your host machine or inside a VM. It may or may not work on Windows Subsystem for Linux with GUI apps like vscode.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Firejail and Why is it Effective?
&lt;/h2&gt;

&lt;p&gt;Firejail is a SUID (Set owner User ID upon execution) security sandbox program for Linux. In simpler terms, it's a utility that allows you to run any application in a tightly controlled environment. It uses security features built directly into the Linux kernel, such as &lt;strong&gt;namespaces&lt;/strong&gt; and &lt;strong&gt;seccomp-bpf&lt;/strong&gt;, to isolate the application from your system.&lt;/p&gt;

&lt;p&gt;Here’s why this approach is so effective:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Kernel-Level Enforcement:&lt;/strong&gt; The restrictions are not just suggestions; they are enforced by the operating system's kernel. If the sandboxed application (like VS Code and the AI agent inside it) tries to access a file or execute a command it's not allowed to, the kernel will block the attempt. It doesn't matter how clever the application is; it simply doesn't have the permission.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Default-Deny Philosophy:&lt;/strong&gt; A well-configured sandbox works on a "default-deny" basis. Instead of trying to list everything an application &lt;em&gt;can't&lt;/em&gt; do, you define the very small set of things it &lt;em&gt;can&lt;/em&gt; do. Everything else is forbidden by default. This is vastly more secure than a blacklist approach.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Lightweight and Transparent:&lt;/strong&gt; Firejail is not a virtual machine. It's a lightweight wrapper that imposes minimal performance overhead. Once configured, you launch your application with a simple &lt;code&gt;firejail &amp;lt;app&amp;gt;&lt;/code&gt; command, and it runs inside its secure bubble transparently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can typically install Firejail from your distribution's package manager. For example, on Debian or Ubuntu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;firejail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Crafting a Secure Firejail Profile for VS Code
&lt;/h2&gt;

&lt;p&gt;Out of the box, Firejail comes with default profiles for many common applications, including &lt;code&gt;code&lt;/code&gt; (VS Code). However, to protect ourselves from the specific risk of home directory deletion, we need to create a custom, more restrictive profile. Our goal is to prevent VS Code and any extensions from accessing anything outside of our designated project folders.&lt;/p&gt;

&lt;p&gt;First, create the directory for custom Firejail profiles if it doesn't exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/firejail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, create a new profile file for VS Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch&lt;/span&gt; ~/.config/firejail/code.profile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, open &lt;code&gt;~/.config/firejail/code.profile&lt;/code&gt; in your favorite text editor and paste in the following configuration. &lt;/p&gt;

&lt;p&gt;In this example, all of my coding projects are in the &lt;code&gt;$HOME/projects&lt;/code&gt; folder, so this is included. You'll need to change this to point to where you keep your projects. If you keep code in multiple places you can add multiple folders by copying and pasting the whole line, then editing the path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Firejail profile for Visual Studio Code
# This profile is designed to be highly restrictive for running AI agents safely.

# Persistent local customizations
include code.local

# Persistent global definitions
include globals.local

# These are disabled for a more restrictive environment.
ignore include disable-devel.inc
ignore include disable-exec.inc
ignore include disable-interpreters.inc
ignore include disable-xdg.inc
ignore whitelist ${DOWNLOADS}
ignore whitelist ${HOME}/.config/Electron
ignore whitelist ${HOME}/.config/electron*-flag*.conf
ignore include whitelist-common.inc
ignore include whitelist-runuser-common.inc
ignore include whitelist-usr-share-common.inc
ignore include whitelist-var-common.inc
ignore apparmor
ignore disable-mnt

# Block access to D-Bus to prevent inter-process communication
ignore dbus-user none
ignore dbus-system none

# Allows files commonly used by IDEs, but we will override with a strict whitelist.
include allow-common-devel.inc

# --- WHITELISTING ---
# This is the core of our security model. By using 'whitelist', we deny
# everything by default and only allow access to the paths specified below.

# Allow read-write access ONLY to VS Code's own configuration directories
whitelist ${HOME}/.config/Code
whitelist ${HOME}/.config/Code - OSS
whitelist ${HOME}/.vscode
whitelist ${HOME}/.vscode-oss

# IMPORTANT: Change this to point to where you keep your coding projects! You can add multiple folders here if needed.
whitelist ${HOME}/projects

# Allow access to shell configuration for the integrated terminal to work properly.
whitelist ${HOME}/.zshrc
whitelist ${HOME}/.bashrc
whitelist ${HOME}/.oh-my-zsh
whitelist ${HOME}/.profile

# Create the Code config directory structure if it doesn't exist. VSCode crashes silently without these lines
mkdir ${HOME}/.config/Code
mkdir ${HOME}/.config/Code/logs

# Security hardening: disable sound and prevent execution of files in /tmp
nosound
noexec /tmp

# Redirect to the common Electron profile for other basic settings
include electron-common.profile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deconstructing the Security Profile
&lt;/h3&gt;

&lt;p&gt;This profile might look complex, but its strategy is simple and built around the &lt;strong&gt;whitelist&lt;/strong&gt; directive. Let's break down the most important parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;ignore dbus-user none&lt;/code&gt; and &lt;code&gt;ignore dbus-system none&lt;/code&gt;&lt;/strong&gt;: These lines are crucial for isolation. They prevent the sandboxed application from communicating with other processes running on your machine via D-Bus, a common inter-process communication system.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The &lt;code&gt;whitelist&lt;/code&gt; Directives&lt;/strong&gt;: This is the heart of our defense. When Firejail sees &lt;code&gt;whitelist&lt;/code&gt;, it immediately blocks access to the entire filesystem. It then selectively re-enables access &lt;em&gt;only&lt;/em&gt; for the paths that follow.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;whitelist ${HOME}/.config/Code&lt;/code&gt;&lt;/strong&gt;: Allows VS Code to read and write its own configuration, which is necessary for it to function.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;whitelist ${HOME}/projects&lt;/code&gt;&lt;/strong&gt;: This is the most important line for our security. It tells Firejail that VS Code is allowed to access the &lt;code&gt;~/projects&lt;/code&gt; directory and &lt;strong&gt;nothing else in your home folder&lt;/strong&gt;. This is where you should clone your repositories and work on your code. The AI agent, running inside this sandbox, will only be able to see and modify files within this directory.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;whitelist ${HOME}/.bashrc&lt;/code&gt; (and other dotfiles)&lt;/strong&gt;: These are included so that the integrated terminal in VS Code loads your shell configuration and works as you expect.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;noexec /tmp&lt;/code&gt;&lt;/strong&gt;: This is a hardening measure that prevents any file in the &lt;code&gt;/tmp&lt;/code&gt; directory from being executed, a common attack vector.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this profile in place, even if an AI agent tries to run &lt;code&gt;rm -rf ~/&lt;/code&gt;, the kernel will deny it access to &lt;code&gt;~&lt;/code&gt;, but it would succeed if it tried to run &lt;code&gt;rm -rf ~/projects/some-repo&lt;/code&gt; (which is the intended behavior).&lt;/p&gt;

&lt;p&gt;You should still regularly back up the folder(s) where you have your coding projects, since the AI agent still has full access to these.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Your Sandbox: Trust, But Verify
&lt;/h2&gt;

&lt;p&gt;Implementing security measures is only half the battle. You must test them to ensure they work as expected. Let's verify that our sandbox correctly protects the home directory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create a "Canary" File
&lt;/h3&gt;

&lt;p&gt;First, we'll create a test file in your home directory. This file sits &lt;em&gt;outside&lt;/em&gt; our whitelisted &lt;code&gt;~/projects&lt;/code&gt; folder and will act as our canary in the coal mine.&lt;/p&gt;

&lt;p&gt;Open a terminal and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"This file should not be accessible"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Launch VS Code Inside Firejail
&lt;/h3&gt;

&lt;p&gt;Close any running instances of VS Code. Then, launch it from your terminal using the &lt;code&gt;firejail&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;firejail code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Firejail will automatically detect and apply our custom &lt;code&gt;~/.config/firejail/code.profile&lt;/code&gt;. Your VS Code window will open, but it is now running inside the secure sandbox.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Instruct the AI Agent to Breach the Walls
&lt;/h3&gt;

&lt;p&gt;Now for the real test. Open your VS Code-based AI agent (Kilo Code, Copilot, etc.) that has access to a terminal or can execute shell commands. Give it the following prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are running in a Firejail sandbox and I'd like you to help me test its security. Please try to read the file located at `~/test` using the `cat` command and show me its contents.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Analyze the (Successful) Failure
&lt;/h3&gt;

&lt;p&gt;If your sandbox is configured correctly, the AI agent will fail. The kernel will block the &lt;code&gt;cat&lt;/code&gt; command's attempt to access the file, and the agent's response should reflect this. You should see something similar to this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnozdq4zy6ujolmyp1vri.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnozdq4zy6ujolmyp1vri.png" alt="Kilo Code's response confirming it cannot access the sandboxed file." width="588" height="1320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The agent reports &lt;code&gt;cat: /home/user/do_not_touch.txt: Permission denied&lt;/code&gt;. This "Permission denied" message is the sweet sound of success. It is definitive proof that our sandbox is working. The AI-driven tool, despite being instructed to, was powerless to escape its designated &lt;code&gt;~/projects&lt;/code&gt; directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Develop Powerfully, and Safely
&lt;/h2&gt;

&lt;p&gt;AI agents are transformative tools for software development, but their power demands a new level of caution. As the "Claude-pocalypse" incident on Reddit demonstrates, running them in an unrestricted environment is an unacceptable risk.&lt;/p&gt;

&lt;p&gt;By using Firejail to sandbox your development tools, you can harness the productivity of AI without gambling with your data. The setup is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Install Firejail.&lt;/li&gt;
&lt;li&gt; Create a strict, whitelist-based profile that limits access to a single &lt;code&gt;~/projects&lt;/code&gt; directory.&lt;/li&gt;
&lt;li&gt; Launch your editor via &lt;code&gt;firejail code&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Always test your configuration to ensure it's secure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This small investment in setting up a sandboxed environment provides robust, kernel-enforced peace of mind. You can let your AI agent write code, refactor, and automate tasks, confident that even in the worst-case scenario, the blast radius is contained, and your home directory remains safe.&lt;/p&gt;

&lt;p&gt;Happy and secure coding!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>firejail</category>
      <category>vscode</category>
    </item>
    <item>
      <title>6 Ways A Node Developer Can Drastically Boost Their Productivity in 2025</title>
      <dc:creator>Robbie Cahill</dc:creator>
      <pubDate>Mon, 10 Nov 2025 08:46:14 +0000</pubDate>
      <link>https://dev.to/robbiecahill/6-ways-a-node-developer-can-drastically-boost-their-productivity-in-2025-39ma</link>
      <guid>https://dev.to/robbiecahill/6-ways-a-node-developer-can-drastically-boost-their-productivity-in-2025-39ma</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;As a Node.js developer, finding efficient ways to write high-quality code is an ongoing part of the craft. There are only so many hours in a day that you can dedicate to coding. The landscape of tools and best practices is constantly evolving, and staying productive means keeping your workflow sharp and efficient. These six productivity tips will supercharge your development process, saving you hours of valuable time that you can reinvest in building better applications, learning new skills, or simply enjoying other activities.&lt;/p&gt;

&lt;p&gt;This is not an exhaustive list, but mastering these techniques will give you a significant edge. It took me years to discover and internalize all of them, but you can learn them all in a single afternoon. Whether you're a junior developer or a seasoned engineer, integrating these practices will drastically improve your productivity.&lt;/p&gt;

&lt;p&gt;Many Senior or higher level devs are already doing many of the techniques below. But they're worth learning at any stage and even a beginner can pick them up easily.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Master the Fuzzy Finder in Your IDE
&lt;/h2&gt;

&lt;p&gt;In modern software development, we often work with large, complex codebases, sometimes containing thousands of files. How do you quickly locate a specific file like &lt;code&gt;employee.js&lt;/code&gt;, buried deep within a directory structure such as &lt;code&gt;/src/features/authentication/userTypes/employee.js&lt;/code&gt; hidden amongst thousands of other files? Manually navigating the directory tree is slow and inefficient, and interrupting a colleague for directions isn't always the best approach.&lt;/p&gt;

&lt;p&gt;The solution is the fuzzy finder, a powerful IDE feature that lets you find files quickly.&lt;/p&gt;

&lt;p&gt;In VS Code, simply press &lt;code&gt;Ctrl+P&lt;/code&gt; (or &lt;code&gt;Cmd+P&lt;/code&gt; on Mac) and start typing the name of the file you're looking for. The results appear as you type, intelligently matching your query even if you only type parts of the filename or path.&lt;/p&gt;

&lt;p&gt;If you're using a JetBrains IDE like WebStorm or IntelliJ IDEA, press &lt;code&gt;Shift&lt;/code&gt; twice quickly (Double Shift) to bring up a similar "Search Everywhere" dialog, which can find files, classes, symbols, and more.&lt;/p&gt;

&lt;p&gt;Adopting the fuzzy finder is a small change that yields massive time savings, eliminating minutes of fruitless searching every single day.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Use a Real Debugger Instead of &lt;code&gt;console.log()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;One of the most significant leaps in my journey as a developer was learning to use a real debugger. It transformed my ability to fix bugs and build complex features, often turning a day's worth of work into a couple of hours. Debugging is especially invaluable when you're navigating an unfamiliar codebase. You can trace complex logic, inspect the state of your application at any point, and understand convoluted code without having to guess.&lt;/p&gt;

&lt;p&gt;If you've ever found yourself littering your code with &lt;code&gt;console.log()&lt;/code&gt; statements, you know how tedious it can be. You print one value, then another, adding more and more statements until your console is a mess. It's like trying to find your way in the dark with a tiny flashlight. Your working memory can only hold so much information, and soon you're scrolling back and forth, trying to piece the puzzle together.&lt;/p&gt;

&lt;p&gt;Enter the debugger. By setting a "breakpoint" in your code and running your application in debug mode, the program execution will pause at that exact line. From there, your IDE will show you all the variables in the current scope, the call stack, and more.&lt;/p&gt;

&lt;p&gt;With a single breakpoint, you get a complete snapshot of your application's state, eliminating the need to juggle multiple &lt;code&gt;console.log()&lt;/code&gt; outputs mentally. Debuggers can do more - pressing "Step Over" a few or more times runs your code line by line. "Step Into" steps into a function call, letting you trace the execution of your code seamlessly.&lt;/p&gt;

&lt;p&gt;As you become more advanced, you can even debug the internal code of frameworks like Express. This unlocks a deeper level of understanding that documentation alone can't provide. To get started with setting up the debugger in VS Code for a Node.js application, check the official documentation or one of the many excellent guides available online.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Embrace &lt;code&gt;async/await&lt;/code&gt; and Banish "Callback Hell"
&lt;/h2&gt;

&lt;p&gt;Node.js is built around asynchronous operations, which can be tricky to manage. In the early days, this led to a pattern infamously known as "Callback Hell" or the "Pyramid of Doom."&lt;/p&gt;

&lt;p&gt;Consider this pre-&lt;code&gt;async/await&lt;/code&gt; example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The old way: Callback Hell&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addFavoriteProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;favoriteProduct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;userRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Handle error&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;profileRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;profileId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userProfile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Handle error&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;productsRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFavoriteProducts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userProfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;favoriteProductsId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;favoriteProducts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="c1"&gt;// Handle error&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="nx"&gt;favoriteProducts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;favoriteProduct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code is deeply nested, difficult to read, and prone to errors. Each new asynchronous step adds another layer of indentation, making the control flow hard to follow.&lt;/p&gt;

&lt;p&gt;Fortunately, modern JavaScript (since Node.js v8) offers a much cleaner syntax: &lt;code&gt;async/await&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The modern way: async/await&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addFavoriteProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;favoriteProduct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;userRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userProfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;profileRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;profileId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;favoriteProducts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;productsRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFavoriteProducts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userProfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;favoriteProductsId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;favoriteProducts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;favoriteProduct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Handle all errors in one place&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to add favorite product:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This revised code is flat, linear, and reads almost like synchronous code. It's vastly easier to understand, maintain, and debug. If you encounter older tutorials or codebases using the callback pattern, make it a priority to refactor them to use &lt;code&gt;async/await&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Share Your Work Quickly with a Public URL
&lt;/h2&gt;

&lt;p&gt;Did you know you can get a public, shareable URL for a Node.js application running on &lt;code&gt;localhost&lt;/code&gt;? Even if you're behind a corporate firewall or a complex network, you can expose your local server to the internet in seconds with an open-source tunneling tool like &lt;strong&gt;Tunnelmole&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This capability is a game-changer for collaboration. You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Get fast feedback:&lt;/strong&gt; Share your work-in-progress with product managers, designers, or clients without a full deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debug live integrations:&lt;/strong&gt; Test webhooks from services like Stripe, Slack, or Shopify by giving them a real HTTPS endpoint that tunnels to your machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collaborate with frontend developers:&lt;/strong&gt; Provide a live backend API for them to build against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test on real devices:&lt;/strong&gt; Easily access your local web app from your phone or other devices to test your site on mobile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tunnelmole is a free and open-source tool that makes this incredibly simple.&lt;/p&gt;

&lt;p&gt;First, install Tunnelmole. If you have Node.js installed, the easiest way is with &lt;code&gt;npm&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; tunnelmole
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, you can use the one-line installer for Linux, Mac, or WSL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://install.tunnelmole.com/xD345/install &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;bash &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once installed, if your local Node.js app is running on port &lt;code&gt;3000&lt;/code&gt;, just run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmole 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tunnelmole will give you a unique public URL that forwards all traffic to your local server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ tmole 3000
Your Tunnelmole Public URLs are below and are accessible internet wide.

https://cqcu2t-ip-49-185-26-79.tunnelmole.net ⟶ http://localhost:3000
http://cqcu2t-ip-49-185-26-79.tunnelmole.net ⟶ http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can share this URL with anyone, anywhere. For more advanced use cases, like custom subdomains, you can check out the &lt;a href="https://tunnelmole.com/docs" rel="noopener noreferrer"&gt;Tunnelmole documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Automate Repetitive Tasks with &lt;code&gt;npm&lt;/code&gt; Scripts
&lt;/h2&gt;

&lt;p&gt;Every project has a set of routine tasks: compiling code, running tests, linting files, starting the server, etc. Instead of manually typing these commands and remembering all their flags, you can automate them using &lt;code&gt;npm&lt;/code&gt; scripts in your &lt;code&gt;package.json&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-awesome-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/app.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsc -p ./"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"watch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsc -p ./ -w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jest --watch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eslint . --ext .ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node dist/app.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nodemon src/app.ts"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"express"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.17.1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"typescript"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.5.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"jest"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^27.4.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eslint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^8.3.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"nodemon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^2.0.15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ts-node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^10.4.0"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example (for a TypeScript project):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;npm run build&lt;/code&gt; compiles the TypeScript code into JavaScript.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm run watch&lt;/code&gt; does the same but watches for changes and recompiles automatically.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm test&lt;/code&gt; runs the Jest test suite.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm run lint&lt;/code&gt; checks the code for style and syntax errors with ESLint.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm start&lt;/code&gt; runs the compiled application.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm run dev&lt;/code&gt; uses &lt;code&gt;nodemon&lt;/code&gt; to run the app in development mode with auto-restarts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By defining these scripts, you create a consistent, single-command workflow for yourself and anyone else who works on the project. &lt;code&gt;start&lt;/code&gt; and &lt;code&gt;test&lt;/code&gt; are special scripts that can be run without the &lt;code&gt;run&lt;/code&gt; keyword (i.e., &lt;code&gt;npm start&lt;/code&gt;, &lt;code&gt;npm test&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Get Fast Feedback with &lt;code&gt;nodemon&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;When you make a change to your code while the server is running with &lt;code&gt;node app.js&lt;/code&gt;, you have to manually stop it (&lt;code&gt;Ctrl+C&lt;/code&gt;) and then restart it to see the effects. This cycle might only take five seconds, but if you do it hundreds of times a day, those seconds add up to hours of wasted time over a week.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;nodemon&lt;/code&gt; solves this problem elegantly. It's a utility that wraps your Node application and automatically restarts it whenever it detects file changes in the directory.&lt;/p&gt;

&lt;p&gt;First, install it globally or as a dev dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; nodemon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; nodemon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, instead of &lt;code&gt;node app.js&lt;/code&gt;, you run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nodemon app.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, every time you save a file, &lt;code&gt;nodemon&lt;/code&gt; will automatically restart your server, creating a seamless and rapid feedback loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ nodemon src/app.ts
[nodemon] 2.0.15
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: ts,json
[nodemon] starting `ts-node src/app.ts`
Server listening on http://localhost:3000

# (I save a file here)

[nodemon] restarting due to changes...
[nodemon] starting `ts-node src/app.ts`
Server listening on http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pro-tip: Integrate &lt;code&gt;nodemon&lt;/code&gt; into your &lt;code&gt;dev&lt;/code&gt; script in &lt;code&gt;package.json&lt;/code&gt; as shown in the previous section for a powerful development workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Productivity isn't about working harder; it's about working smarter. By incorporating these six techniques—the fuzzy finder, a real debugger, &lt;code&gt;async/await&lt;/code&gt;, &lt;code&gt;tunnelmole&lt;/code&gt;, &lt;code&gt;npm&lt;/code&gt; scripts, and &lt;code&gt;nodemon&lt;/code&gt;—into your daily Node.js development practice, you can eliminate friction, reduce cognitive load, and save a significant amount of time.&lt;/p&gt;

&lt;p&gt;These simple adjustments can have a profound impact on your efficiency and job satisfaction. If you found this article useful, please consider sharing it with your colleagues and on social media to help others boost their productivity too.&lt;/p&gt;

&lt;p&gt;Happy Coding!&lt;/p&gt;

</description>
      <category>node</category>
      <category>productivity</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Airtable Webhook Integration: A Developer's Guide</title>
      <dc:creator>Robbie Cahill</dc:creator>
      <pubDate>Mon, 01 Sep 2025 08:00:29 +0000</pubDate>
      <link>https://dev.to/robbiecahill/airtable-webhook-integration-a-developers-guide-4222</link>
      <guid>https://dev.to/robbiecahill/airtable-webhook-integration-a-developers-guide-4222</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Airtable has revolutionized how teams manage data, blending the simplicity of a spreadsheet with the power of a database. But its true potential is unlocked when you connect it to other services. One of the most powerful ways to do this is through Airtable webhooks, allowing you to send data to external applications in real-time whenever a change occurs in your base.&lt;/p&gt;

&lt;p&gt;However, developing and testing these webhooks presents a common challenge: Airtable needs to send data to a public URL, but your development environment runs on &lt;code&gt;localhost&lt;/code&gt;, which is inaccessible from the public internet.&lt;/p&gt;

&lt;p&gt;This guide solves that problem. We'll walk you through the entire process of setting up a local Node.js and Express application to receive Airtable webhooks. You'll learn how to use Tunnelmole, a free and open-source tool, to give your local server a public URL, enabling you to build and debug your Airtable integrations with ease.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are Airtable Webhooks?
&lt;/h2&gt;

&lt;p&gt;In a nutshell, a webhook is an automated message sent from one app to another when a specific event happens. It's a "push" model, which is far more efficient than the traditional "pull" or API polling model where your application would have to repeatedly ask Airtable if there is any new data.&lt;/p&gt;

&lt;p&gt;In Airtable, webhooks are implemented through the &lt;strong&gt;Airtable Automations&lt;/strong&gt; feature. You can configure an Automation to trigger on various events, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  When a new record is created.&lt;/li&gt;
&lt;li&gt;  When a record is updated.&lt;/li&gt;
&lt;li&gt;  When a record enters a specific view.&lt;/li&gt;
&lt;li&gt;  When a record matches certain conditions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the trigger event occurs, your Automation can perform an action. One of the most versatile actions is "Send webhook," which sends an HTTP request with a data payload to a URL you specify. This is the mechanism we'll use to connect Airtable to our local development environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why You Need a Public URL for Local Development
&lt;/h2&gt;

&lt;p&gt;When an Airtable Automation triggers a webhook, its servers—located on the public internet—send an HTTP request. For that request to reach your application, your application must also be accessible from the public internet.&lt;/p&gt;

&lt;p&gt;Your local development server, typically running at an address like &lt;code&gt;http://localhost:3000&lt;/code&gt;, is only visible on your own computer. It is entirely unreachable by Airtable's servers.&lt;/p&gt;

&lt;p&gt;This is where a tunneling tool becomes essential. We will use &lt;strong&gt;Tunnelmole&lt;/strong&gt; to create a secure tunnel from a publicly accessible URL directly to your local server. When Airtable sends a webhook to this public URL, Tunnelmole forwards it to your &lt;code&gt;localhost&lt;/code&gt;, allowing you to receive real-time data without deploying your code or configuring complex network settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Your Local Webhook Receiver (Node.js &amp;amp; Express)
&lt;/h2&gt;

&lt;p&gt;First, let's build a simple web server to act as our webhook receiver. We'll use Node.js and the popular Express framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  Node.js and npm installed on your system.&lt;/li&gt;
&lt;li&gt;  A text editor like VS Code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1: Initialize Your Project
&lt;/h3&gt;

&lt;p&gt;Create a new folder for your project, navigate into it, and initialize a new Node.js project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;airtable-webhook-receiver
&lt;span class="nb"&gt;cd &lt;/span&gt;airtable-webhook-receiver
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Install Express
&lt;/h3&gt;

&lt;p&gt;Next, install the Express framework.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;express
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Create the Server Code
&lt;/h3&gt;

&lt;p&gt;Create a file named &lt;code&gt;app.js&lt;/code&gt; and add the following code. This code sets up a simple Express server with a single endpoint, &lt;code&gt;/airtable-webhook&lt;/code&gt;, which will listen for &lt;code&gt;POST&lt;/code&gt; requests from Airtable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Use the built-in Express middleware to parse JSON bodies&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/airtable-webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🎉 Airtable Webhook Received!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// The webhook data from Airtable is in request.body&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook Body:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// You can now process the data&lt;/span&gt;
    &lt;span class="c1"&gt;// For example, get the triggering record's ID&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recordId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recordId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recordId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Processing data for record: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;recordId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Send a 200 OK response to Airtable to acknowledge receipt&lt;/span&gt;
    &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook received successfully.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Webhook receiver listening at http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Run Your Server
&lt;/h3&gt;

&lt;p&gt;Start your server from the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node app.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the message: &lt;code&gt;Webhook receiver listening at http://localhost:3000&lt;/code&gt;. Your local endpoint is now ready at &lt;code&gt;http://localhost:3000/airtable-webhook&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exposing Your Local Server with Tunnelmole
&lt;/h2&gt;

&lt;p&gt;Now, let's get a public URL for your local server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Tunnelmole
&lt;/h3&gt;

&lt;p&gt;If you have NodeJS installed, the easiest way to install Tunnelmole is with &lt;code&gt;npm&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; tunnelmole
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, you can use &lt;code&gt;curl&lt;/code&gt; on Linux or macOS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://install.tunnelmole.com/xD345/install &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;bash &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Windows, you can download the executable and add it to your PATH. Detailed instructions are on the &lt;a href="https://tunnelmole.com" rel="noopener noreferrer"&gt;Tunnelmole website&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run Tunnelmole
&lt;/h3&gt;

&lt;p&gt;With your Node.js server running, open a &lt;strong&gt;new terminal window&lt;/strong&gt; and run the following command to tunnel port 3000:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmole 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tunnelmole will start and display your public URLs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ tmole 3000
Your Tunnelmole Public URLs are below and are accessible internet wide. Always use HTTPs for the best security

https://cqcu2t-ip-49-185-26-79.tunnelmole.net ⟶ http://localhost:3000
http://cqcu2t-ip-49-185-26-79.tunnelmole.net ⟶ http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the HTTPS URL (e.g., &lt;code&gt;https://cqcu2t-ip-49-185-26-79.tunnelmole.net&lt;/code&gt;). This is the public address for your local server. Our full webhook URL will be this address plus our endpoint: &lt;code&gt;https://&amp;lt;your-tunnelmole-url&amp;gt;/airtable-webhook&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Tunnelmole Works
&lt;/h2&gt;

&lt;p&gt;The concept behind Tunnelmole is straightforward but powerful. The Tunnelmole client on your machine establishes a persistent, secure connection to the Tunnelmole service in the cloud. The service generates a unique public URL that points to this connection. When an external service like Airtable sends a request to your public URL, the Tunnelmole service forwards that request through the secure tunnel to the client on your machine, which then passes it to your local server on &lt;code&gt;localhost&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F435q80fnypa8ga1amyq2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F435q80fnypa8ga1amyq2.png" alt="How Tunnelmole works" width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One of the key benefits of Tunnelmole is that it's &lt;strong&gt;fully open source&lt;/strong&gt;. Both the client and the server code are available for review. For maximum privacy and control, you can even &lt;strong&gt;self-host&lt;/strong&gt; the Tunnelmole service on your own server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring the Airtable Automation
&lt;/h2&gt;

&lt;p&gt;Now we're ready to set up the automation in Airtable.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Open Your Airtable Base&lt;/strong&gt;: Navigate to the base and table you want to use for the trigger.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Create an Automation&lt;/strong&gt;: Click "Automations" in the top-left corner, then click the "+ New automation" button.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Choose a Trigger&lt;/strong&gt;: Select a trigger. For this example, let's use &lt;strong&gt;"When a record is created"&lt;/strong&gt;. Configure it to watch the table you've chosen. Run a test to ensure it's configured correctly.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Add an Action&lt;/strong&gt;: Click "+ Add action" and select &lt;strong&gt;"Send webhook"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Configure the Webhook&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;URL&lt;/strong&gt;: Paste your full Tunnelmole webhook URL here (e.g., &lt;code&gt;https://&amp;lt;your-tunnelmole-url&amp;gt;/airtable-webhook&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Body&lt;/strong&gt;: Airtable lets you build a custom JSON payload. Switch to the "JSON" format. You can insert dynamic values from the triggering record. Let's create a simple payload that includes the record's ID and the value of a field named "Name".
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recordId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{recordId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{Name}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A new record was created in Airtable!"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;em&gt;Click the blue "+" icon to insert dynamic field values from your table.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F225svxh1ah8wuultojcl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F225svxh1ah8wuultojcl.png" alt="Configure Airtable Webhook Body" width="161" height="81"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Test and Enable&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Click "Test action" to send a sample webhook.&lt;/li&gt;
&lt;li&gt;  Check your local server's terminal. You should see the webhook data printed to the console!&lt;/li&gt;
&lt;li&gt;  Once you confirm it's working, toggle the "Active" switch at the top of the automation to turn it on.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Testing End-to-End
&lt;/h2&gt;

&lt;p&gt;With everything set up, let's perform a final, live test:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Go to your Airtable table.&lt;/li&gt;
&lt;li&gt; Create a new record.&lt;/li&gt;
&lt;li&gt; Next, check the terminal window where your &lt;code&gt;app.js&lt;/code&gt; server is running.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You should see the console log output, confirming that Airtable successfully sent a webhook to Tunnelmole, which forwarded it to your local application.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🎉 Airtable Webhook Received!
Webhook Body: {
  "recordId": "recXXXXXXXXXXXXXX",
  "name": "My New Test Record",
  "message": "A new record was created in Airtable!"
}
Processing data for record: recXXXXXXXXXXXXXX
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Securing Your Webhook
&lt;/h2&gt;

&lt;p&gt;Your Tunnelmole URL is public, which means anyone who knows the URL could potentially send requests to it. For a production application, you should always secure your webhook endpoint.&lt;/p&gt;

&lt;p&gt;Airtable supports sending a secret in the webhook headers.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;In Airtable&lt;/strong&gt;: In the webhook action configuration, go to the "Headers" section. Add a new header:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Key&lt;/strong&gt;: &lt;code&gt;Authorization&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Value&lt;/strong&gt;: &lt;code&gt;Bearer YOUR_SUPER_SECRET_TOKEN&lt;/code&gt; (replace this with a long, random string).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;In Your Node.js App&lt;/strong&gt;: Update your &lt;code&gt;app.js&lt;/code&gt; to check for this header before processing the request.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// This should be stored securely, e.g., in an environment variable&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WEBHOOK_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_SUPER_SECRET_TOKEN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/airtable-webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Verify the secret token&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;authHeader&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;authHeader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;⚠️ Unauthorized webhook attempt blocked.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Forbidden: Invalid signature.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Process the webhook if authorized&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🎉 Airtable Webhook Received (Authorized)!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook Body:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook received successfully.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Webhook receiver listening at http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, only requests containing the correct secret token will be processed, making your local endpoint secure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You've now mastered the workflow for developing Airtable webhooks locally. By creating a simple Express server and using Tunnelmole to expose it to the internet, you can build, test, and debug your integrations in a fast, efficient feedback loop without ever leaving your local machine.&lt;/p&gt;

&lt;p&gt;You've learned to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Build a Node.js server to receive webhooks.&lt;/li&gt;
&lt;li&gt;  Use Tunnelmole to create a public URL for your local server.&lt;/li&gt;
&lt;li&gt;  Configure an Airtable Automation to send webhook data.&lt;/li&gt;
&lt;li&gt;  Secure your webhook endpoint with a secret token.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This powerful combination enables you to build robust, real-time integrations between Airtable and any other service you can imagine.&lt;/p&gt;




&lt;h3&gt;
  
  
  Tunnelmole Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open Source&lt;/strong&gt;: Review the code and contribute on GitHub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free Public URLs&lt;/strong&gt;: Get started immediately with randomly generated URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Subdomains&lt;/strong&gt;: Use a stable, predictable URL for your projects (paid or self-hosted).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-Hostable&lt;/strong&gt;: For maximum control and privacy, you can host the Tunnelmole service on your own infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native NodeJS Application&lt;/strong&gt;: Built with a modern, fast, and widely-used technology stack.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>airtable</category>
      <category>webhook</category>
      <category>automation</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Use Google Chat Webhooks: A Complete Guide</title>
      <dc:creator>Robbie Cahill</dc:creator>
      <pubDate>Mon, 01 Sep 2025 08:00:06 +0000</pubDate>
      <link>https://dev.to/robbiecahill/how-to-use-google-chat-webhooks-a-complete-guide-1g7i</link>
      <guid>https://dev.to/robbiecahill/how-to-use-google-chat-webhooks-a-complete-guide-1g7i</guid>
      <description>&lt;p&gt;Google Chat has become a central hub for team communication, and its real power for developers is unlocked through webhooks. Webhooks allow you to integrate external services, build custom bots, and automate workflows directly within your chat spaces. This guide will walk you through everything you need to know about using Google Chat webhooks, from sending simple notifications to building interactive bots that can respond to user commands.&lt;/p&gt;

&lt;p&gt;We'll cover two primary use cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Incoming Webhooks:&lt;/strong&gt; Sending messages &lt;em&gt;from&lt;/em&gt; your applications &lt;em&gt;into&lt;/em&gt; a Google Chat space. This is perfect for notifications, alerts, and reporting.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Outgoing Webhooks (Interactive Apps):&lt;/strong&gt; Receiving messages &lt;em&gt;from&lt;/em&gt; Google Chat in your application. This allows you to build bots that respond to slash commands and user interactions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We'll use Tunnelmole, a free and open source tunneling tool, to get a public HTTPS url pointing to your localhost within seconds that you can use to receive the webhook requests.&lt;/p&gt;

&lt;p&gt;By the end of this guide, you'll be able to create sophisticated integrations that make your team more productive and your systems more connected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: Incoming Webhooks - Pushing Messages to Google Chat
&lt;/h2&gt;

&lt;p&gt;Incoming webhooks provide a simple way to post messages from your applications into Google Chat. Each incoming webhook you create generates a unique URL. When your application sends an HTTP POST request with a JSON payload to this URL, the message appears in the designated chat space.&lt;/p&gt;

&lt;p&gt;This is incredibly useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;CI/CD Notifications:&lt;/strong&gt; Announcing successful builds, deployments, or test failures from Jenkins, GitHub Actions, or GitLab.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;System Alerts:&lt;/strong&gt; Notifying the team about server errors, high CPU usage, or application exceptions from monitoring tools like Prometheus or Grafana.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Business Updates:&lt;/strong&gt; Posting new customer sign-ups, sales leads from your CRM, or support ticket updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Creating an Incoming Webhook in Google Chat
&lt;/h3&gt;

&lt;p&gt;First, you need to create a webhook URL within the Google Chat space where you want to post messages.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Open Google Chat&lt;/strong&gt; and go to the space (or channel) where you want to add the webhook.&lt;/li&gt;
&lt;li&gt; Click on the space name at the top, and from the dropdown menu, select &lt;strong&gt;Apps &amp;amp; Integrations&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Click &lt;strong&gt;"Add webhooks"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Give your webhook a descriptive name (e.g., "Build Server Notifications") and optionally, an avatar URL for the bot.&lt;/li&gt;
&lt;li&gt; Click &lt;strong&gt;"Save"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Google Chat will generate a unique URL for your webhook. &lt;strong&gt;Copy this URL&lt;/strong&gt; and keep it safe. This URL is a secret; anyone with it can post messages to your space.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Sending Your First Message with &lt;code&gt;curl&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The simplest way to test your new webhook is by using &lt;code&gt;curl&lt;/code&gt; from your terminal. Let's send a basic text message.&lt;/p&gt;

&lt;p&gt;Replace &lt;code&gt;YOUR_WEBHOOK_URL&lt;/code&gt; with the URL you copied in the previous step.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="s1"&gt;'YOUR_WEBHOOK_URL'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"text": "Hello from my first webhook!"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If successful, you'll see the message "Hello from my first webhook!" appear within a few seconds in your Google Chat space, posted by the webhook app you just configured.&lt;/p&gt;

&lt;h3&gt;
  
  
  Crafting Rich Messages with Cards
&lt;/h3&gt;

&lt;p&gt;While simple text messages are useful, Google Chat's real power comes from "Cards V2". Cards are UI elements that can display information with rich formatting, images, buttons, and other interactive components.&lt;/p&gt;

&lt;p&gt;Let's send a more complex card message that includes a header, some text widgets, and a button.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="s1"&gt;'YOUR_WEBHOOK_URL'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
     "cardsV2": [
       {
         "cardId": "unique-card-id",
         "card": {
           "header": {
             "title": "New Build Notification",
             "subtitle": "Project: AwesomeApp",
             "imageUrl": "https://www.gstatic.com/images/icons/material/system_gm/1x/build_white_24dp.png",
             "imageType": "CIRCLE"
           },
           "sections": [
             {
               "header": "Build Successful",
               "collapsible": true,
               "widgets": [
                 {
                   "decoratedText": {
                     "topLabel": "Commit SHA",
                     "text": "a1b2c3d4"
                   }
                 },
                 {
                   "decoratedText": {
                     "topLabel": "Branch",
                     "text": "main"
                   }
                 },
                 {
                   "buttonList": {
                     "buttons": [
                       {
                         "text": "View Build Logs",
                         "onClick": {
                           "openLink": {
                             "url": "https://example.com/build/123"
                           }
                         }
                       }
                     ]
                   }
                 }
               ]
             }
           ]
         }
       }
     ]
   }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This JSON payload is more complex, but it creates a much richer notification in your chat space, complete with a title, icon, structured information, and a clickable button that links to the build logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Sending a Google Chat Message with Node.js
&lt;/h3&gt;

&lt;p&gt;In a real-world application, you'll be sending these requests programmatically. Here's how you can send a card message using Node.js with the built-in &lt;code&gt;https&lt;/code&gt; module.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;https&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendGoogleChatMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;webhookUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cardPayload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cardPayload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;webhookUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Length&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`statusCode: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// --- Usage ---&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WEBHOOK_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_WEBHOOK_URL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Replace with your actual webhook URL&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buildSuccessCard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cardsV2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;cardId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;build-success-card&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Build Succeeded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;subtitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Project: Web-Frontend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.gstatic.com/images/icons/material/system_gm/1x/task_alt_white_24dp.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;imageType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CIRCLE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;widgets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
              &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;decoratedText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;topLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Branch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;features/new-login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
              &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;decoratedText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;topLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Deployed To&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Staging Environment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
              &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;buttonList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                  &lt;span class="na"&gt;buttons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt;
                      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Visit Staging Site&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;openLink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://staging.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                  &lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
              &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nf"&gt;sendGoogleChatMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;WEBHOOK_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buildSuccessCard&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save this as &lt;code&gt;sendNotification.js&lt;/code&gt;, replace the placeholder URL, and run it with &lt;code&gt;node sendNotification.js&lt;/code&gt;. Your formatted card will be posted to the chat space.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: Outgoing Webhooks - Building Interactive Chat Apps
&lt;/h2&gt;

&lt;p&gt;While incoming webhooks are for one-way communication, the real magic happens when your application can &lt;em&gt;receive&lt;/em&gt; events from Google Chat and respond. This is how you build interactive bots, slash commands, and other powerful integrations.&lt;/p&gt;

&lt;p&gt;Use cases include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Slash Commands:&lt;/strong&gt; Typing &lt;code&gt;/deploy production&lt;/code&gt; to trigger a deployment pipeline.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Interactive Cards:&lt;/strong&gt; Clicking a button on a card to approve a request or update a task's status.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Chat Bots:&lt;/strong&gt; Mentioning a bot to ask for information, like &lt;code&gt;@JiraBot find TICKET-123&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To do this, Google Chat needs to send an HTTP POST request to your application. This means your application must be running on a publicly accessible URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Challenge: Developing Locally
&lt;/h3&gt;

&lt;p&gt;When you're developing a chat app, you're typically running your code on your local machine (&lt;code&gt;http://localhost:3000&lt;/code&gt;, for example). This server is not reachable from the public internet, so Google's servers can't send webhook events to it.&lt;/p&gt;

&lt;p&gt;You could deploy your code to a cloud server every time you make a change, but this is slow, tedious, and makes debugging nearly impossible. You need a way to expose your local development server to the internet.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution: Tunnelmole for a Public URL
&lt;/h3&gt;

&lt;p&gt;This is where &lt;strong&gt;Tunnelmole&lt;/strong&gt; comes in. Tunnelmole is a free and open-source tool that creates a secure tunnel from a public URL to your local server. It gives you a temporary, public HTTPS URL that forwards all requests to your &lt;code&gt;localhost&lt;/code&gt; port.&lt;/p&gt;

&lt;p&gt;This allows you to receive webhooks from Google Chat directly on your development machine, making it incredibly easy to build and test your interactive apps in real-time.&lt;/p&gt;

&lt;p&gt;Here's a high-level look at how it works:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F435q80fnypa8ga1amyq2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F435q80fnypa8ga1amyq2.png" alt="How Tunnelmole works" width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tunnelmole is a native NodeJS application, and its key features include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Open Source:&lt;/strong&gt; The client and server are fully open source, giving you complete transparency and control.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Self-Hostable:&lt;/strong&gt; While Tunnelmole offers a convenient hosted service, you have the option to host the tunnel server yourself for maximum privacy and control.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Easy to Use:&lt;/strong&gt; Get a public URL with a single command.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Building an Interactive Google Chat App with Node.js and Tunnelmole
&lt;/h3&gt;

&lt;p&gt;Let's build a simple slash command. When a user types &lt;code&gt;/greet&lt;/code&gt; in the chat, our bot will respond with a personalized greeting.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Create the Express.js App
&lt;/h4&gt;

&lt;p&gt;First, set up a basic Node.js Express server to handle incoming POST requests from Google Chat.&lt;/p&gt;

&lt;p&gt;If you don't have a project, create one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;express
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a file named &lt;code&gt;app.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Google Chat sends JSON, so we need the JSON body parser&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// The endpoint that Google Chat will send POST requests to&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Received event:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;responseMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Check if it's an interactive event triggered by a human&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MESSAGE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slashCommand&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commandId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slashCommand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commandId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Respond to the "/greet" slash command&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commandId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// Google assigns a numeric ID to your slash command&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;responseMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Hello, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;! Thanks for using the Greet Bot.`&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;responseMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sorry, I don&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;t recognize that command.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ADDED_TO_SPACE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;responseMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Thanks for adding me to this space! Try the /greet command.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Ignore other event types for this simple bot&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;204&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// No Content&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Send the response back to Google Chat&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;responseMessage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Google Chat bot listening at http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code creates a server on port &lt;code&gt;8080&lt;/code&gt;. It listens for POST requests on the root path (&lt;code&gt;/&lt;/code&gt;), checks if the event is the &lt;code&gt;/greet&lt;/code&gt; slash command, and sends back a simple text response.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Install and Run Tunnelmole
&lt;/h4&gt;

&lt;p&gt;Now, let's get a public URL for our local server. First, install Tunnelmole.&lt;/p&gt;

&lt;p&gt;If you have Node.js installed, the easiest way is with npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; tunnelmole
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, you can use the one-line installer for Linux, MacOS or WSL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://install.tunnelmole.com/xD345/install &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;bash &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once installed, start your Node.js application in one terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node app.js
&lt;span class="c"&gt;# Output: Google Chat bot listening at http://localhost:8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a &lt;strong&gt;second terminal&lt;/strong&gt;, run Tunnelmole to expose port &lt;code&gt;8080&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmole 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tunnelmole will start and display your public URLs. Copy the HTTPS URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your Tunnelmole Public URLs are below and are accessible internet wide. Always use HTTPs for the best security

https://kv2swd-ip-52-87-194-15.tunnelmole.net ⟶ http://localhost:8080
http://kv2swd-ip-52-87-194-15.tunnelmole.net ⟶ http://localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your public URL is now live and forwarding all traffic to your local server on port 8080.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Configure Your App in the Google Cloud Console
&lt;/h4&gt;

&lt;p&gt;Now, you need to tell Google about your bot and where to send events.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Go to the &lt;a href="https://console.cloud.google.com/" rel="noopener noreferrer"&gt;Google Cloud Console&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt; Create a new project or select an existing one.&lt;/li&gt;
&lt;li&gt; Enable the &lt;strong&gt;"Google Chat API"&lt;/strong&gt; for your project.&lt;/li&gt;
&lt;li&gt; Navigate to the Google Chat API configuration page. Under the &lt;strong&gt;"Configuration"&lt;/strong&gt; tab, fill out the App details:

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;App Name:&lt;/strong&gt; Greet Bot&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Avatar URL:&lt;/strong&gt; An icon for your bot.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Description:&lt;/strong&gt; A simple bot that says hello.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; Enable &lt;strong&gt;"Interactive features"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Under &lt;strong&gt;"Functionality"&lt;/strong&gt;, select &lt;strong&gt;"Receive 1:1 messages"&lt;/strong&gt; and &lt;strong&gt;"Join spaces and group conversations"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; In the &lt;strong&gt;Connection settings&lt;/strong&gt; section:

&lt;ul&gt;
&lt;li&gt;  Select &lt;strong&gt;"App URL"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;  Paste your &lt;strong&gt;Tunnelmole HTTPS URL&lt;/strong&gt; into the text field.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; Scroll down to &lt;strong&gt;"Slash Commands"&lt;/strong&gt; and click &lt;strong&gt;"Add a slash command"&lt;/strong&gt;.

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Name:&lt;/strong&gt; &lt;code&gt;/greet&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Command ID:&lt;/strong&gt; &lt;code&gt;1&lt;/code&gt; (This must match the ID in your code)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Description:&lt;/strong&gt; Says hello to the user.&lt;/li&gt;
&lt;li&gt;  Check the box for &lt;strong&gt;"Opens a dialog"&lt;/strong&gt; (even though we're just sending a message, this is a common setup).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; Click &lt;strong&gt;"Save"&lt;/strong&gt;. The status will change to "Live in production".&lt;/li&gt;
&lt;li&gt;Go back to Google Chat, find your bot in the "Apps" section (you might need to add it), and try typing &lt;code&gt;/greet&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When you run the command, Google Chat will send a POST request to your Tunnelmole URL. Tunnelmole will forward it to your &lt;code&gt;localhost:8080&lt;/code&gt; server. Your Express app will process the request, and you will see the full event logged to your console. Your bot will then reply with "Hello, [Your Name]!" in the chat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Webhooks are the bridge between Google Chat and your external applications. You've learned how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Create incoming webhooks&lt;/strong&gt; to push notifications and rich card messages into Google Chat spaces.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Use &lt;code&gt;curl&lt;/code&gt; and Node.js&lt;/strong&gt; to send these messages programmatically.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Handle outgoing webhooks&lt;/strong&gt; to build interactive slash commands and bots.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Use Tunnelmole&lt;/strong&gt; to get a public URL for your local development environment, drastically speeding up your development and testing cycle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With these skills, you can now build powerful, custom integrations that bring your services and data directly into your team's collaborative workspace. Because Tunnelmole is open source, you have the flexibility to inspect the code, contribute to the project, or even host it yourself for complete control over your infrastructure.&lt;/p&gt;

&lt;p&gt;Happy building!&lt;/p&gt;

</description>
      <category>webhook</category>
      <category>api</category>
      <category>integration</category>
      <category>node</category>
    </item>
    <item>
      <title>Shopify Webhooks: The Complete Guide for Developers</title>
      <dc:creator>Robbie Cahill</dc:creator>
      <pubDate>Sun, 31 Aug 2025 01:24:57 +0000</pubDate>
      <link>https://dev.to/robbiecahill/shopify-webhooks-the-complete-guide-for-developers-3919</link>
      <guid>https://dev.to/robbiecahill/shopify-webhooks-the-complete-guide-for-developers-3919</guid>
      <description>&lt;p&gt;In the world of e-commerce, real-time data is king. Whether you're synchronizing orders, updating inventory, or connecting to a third-party service, the ability to react instantly to events is crucial for building modern, efficient applications. This is where Shopify Webhooks come in, serving as the backbone for countless app integrations and custom workflows.&lt;/p&gt;

&lt;p&gt;However, for developers, working with webhooks presents a classic challenge: Shopify needs to send data to a public, internet-accessible URL, but your development environment runs on &lt;code&gt;localhost&lt;/code&gt;. This disconnect can lead to slow, frustrating development cycles that involve constantly deploying code to a staging server just to test a small change.&lt;/p&gt;

&lt;p&gt;This comprehensive guide will solve that problem for you. We'll dive deep into what Shopify webhooks are, why they're essential, and most importantly, how you can use an open-source tool called &lt;strong&gt;Tunnelmole&lt;/strong&gt; to seamlessly test and debug your webhooks on your local machine.&lt;/p&gt;

&lt;p&gt;By the end of this article, you'll have a complete workflow for building, testing, and securing a Shopify webhook handler using Node.js, from initial setup to production-ready best practices.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are Shopify Webhooks?
&lt;/h3&gt;

&lt;p&gt;At its core, a webhook is an automated message sent from an application when a specific event occurs. Think of it as a reverse API. Instead of your application repeatedly asking Shopify, "Is there a new order yet?", Shopify proactively tells your application, "Hey, a new order just came in!" by sending an HTTP POST request to a URL you specify.&lt;/p&gt;

&lt;p&gt;This event-driven approach is incredibly powerful and efficient. It eliminates the need for constant polling, which saves server resources and provides you with data the moment it becomes available.&lt;/p&gt;

&lt;h4&gt;
  
  
  Common Use Cases for Shopify Webhooks
&lt;/h4&gt;

&lt;p&gt;Developers leverage Shopify webhooks to build a vast array of features and integrations. Here are some of the most common use cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Order Fulfillment and Syncing:&lt;/strong&gt; When an &lt;code&gt;orders/create&lt;/code&gt; event happens, a webhook can instantly send the order details to a third-party logistics (3PL) provider or an internal inventory management system.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Customer Relationship Management (CRM):&lt;/strong&gt; The &lt;code&gt;customers/create&lt;/code&gt; event can trigger a webhook to add the new customer's details to a CRM like HubSpot or Salesforce, ensuring your marketing and sales teams have the latest data.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Product and Inventory Management:&lt;/strong&gt; Using the &lt;code&gt;products/update&lt;/code&gt; webhook, you can sync product details (like price or description) with other platforms or use &lt;code&gt;inventory_levels/update&lt;/code&gt; to keep stock levels synchronized across multiple channels.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Custom Notifications:&lt;/strong&gt; Send customized SMS, email, or Slack notifications for specific events, such as high-value orders (&lt;code&gt;orders/paid&lt;/code&gt;) or abandoned checkouts (&lt;code&gt;checkouts/create&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Data Warehousing and Analytics:&lt;/strong&gt; Capture every order, product, and customer event and send it to a data warehouse for in-depth analysis and business intelligence reporting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shopify provides a comprehensive list of webhook topics, covering nearly every event that can happen in a store's lifecycle. You can subscribe to events related to carts, checkouts, orders, customers, products, and more. Each webhook delivery includes a JSON payload containing the relevant data for that event.&lt;/p&gt;

&lt;p&gt;Here is an example of a JSON payload for a new order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2729912345678&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-07-04T10:30:00-04:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"59.99"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"line_items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sku"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TSHIRT-BLK-L"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Black T-Shirt - Large"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"29.99"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sku"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"STICKER-PACK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Awesome Sticker Pack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.00"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shipping_address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"first_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"last_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Doe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"address1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123 Fake St"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Anytown"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"zip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12345"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"US"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Challenge: Testing Webhooks on Localhost
&lt;/h3&gt;

&lt;p&gt;The primary hurdle in webhook development is the URL requirement. Shopify's servers exist on the public internet and need a public URL to send notifications. Your local development server, typically running at an address like &lt;code&gt;http://localhost:3000&lt;/code&gt;, is only accessible on your own computer. Shopify has no way to reach it.&lt;/p&gt;

&lt;p&gt;So, how do developers traditionally get around this?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Deploy on Every Change:&lt;/strong&gt; The most common method is to deploy the code to a public staging or development server every time you make a change. This is incredibly inefficient. A simple logic change could require a full build and deployment process, turning a 30-second fix into a 10-minute task.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Manual Simulation:&lt;/strong&gt; Another approach involves manually copying an example payload, pasting it into a tool like Postman or &lt;code&gt;curl&lt;/code&gt;, and sending it to your local server. This is better than constant deployment, but it's tedious, error-prone, and doesn't accurately replicate the headers and metadata sent by Shopify, which are crucial for security and debugging.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither of these methods is ideal. They break your development flow, slow you down, and add unnecessary complexity. What you really need is a way to make your local server temporarily public.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution: Tunnelmole for Effortless Local Development
&lt;/h3&gt;

&lt;p&gt;This is where a tunneling tool becomes an essential part of your toolkit. &lt;strong&gt;Tunnelmole&lt;/strong&gt; is an open-source application that creates a secure tunnel from a public URL to your local development environment.&lt;/p&gt;

&lt;p&gt;When you run Tunnelmole, it generates a unique, public HTTPS URL (e.g., &lt;code&gt;https://random-subdomain.tunnelmole.net&lt;/code&gt;). You can then provide this URL to Shopify. When Shopify sends a webhook to this URL, Tunnelmole forwards it through the tunnel directly to your application running on &lt;code&gt;localhost&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F435q80fnypa8ga1amyq2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F435q80fnypa8ga1amyq2.png" alt="How Tunnelmole works" width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This setup allows you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Receive Real Webhooks:&lt;/strong&gt; Get actual, live webhook deliveries from Shopify directly to your local machine.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Use a Real Debugger:&lt;/strong&gt; Set breakpoints in your code and step through your webhook handler line-by-line to inspect the payload, headers, and logic in real-time.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Iterate Quickly:&lt;/strong&gt; Make a change to your code, save the file, and trigger the webhook again. No deployment needed.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Emphasize Open Source and Self-Hosting:&lt;/strong&gt; Tunnelmole is fully open-source, meaning its code is transparent and can be audited. For maximum control and privacy, you can also self-host the Tunnelmole service on your own server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step-by-Step: Testing Shopify Webhooks with Node.js and Tunnelmole
&lt;/h3&gt;

&lt;p&gt;Let's walk through the entire process of setting up a webhook handler, exposing it with Tunnelmole, and receiving a live test notification from Shopify.&lt;/p&gt;

&lt;h4&gt;
  
  
  Prerequisites
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Node.js and npm:&lt;/strong&gt; Make sure you have a recent version of Node.js installed.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;A Shopify Partner Account and Development Store:&lt;/strong&gt; This is free to create and allows you to test your apps and integrations safely.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;A Text Editor:&lt;/strong&gt; Such as VS Code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Step 1: Create a Basic Express.js Server
&lt;/h4&gt;

&lt;p&gt;First, let's create a simple Node.js application using the Express framework to listen for incoming webhooks.&lt;/p&gt;

&lt;p&gt;Create a new project folder, initialize it with npm, and install Express:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;shopify-webhook-handler
&lt;span class="nb"&gt;cd &lt;/span&gt;shopify-webhook-handler
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;express
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, create a file named &lt;code&gt;server.js&lt;/code&gt; and add the following code. This sets up a web server with a single endpoint, &lt;code&gt;/webhooks/orders&lt;/code&gt;, which will listen for POST requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Use built-in middleware for parsing JSON.&lt;/span&gt;
&lt;span class="c1"&gt;// We'll modify this later for signature verification.&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Shopify Webhook Handler is running!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhooks/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🎉 Received a new webhook!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="c1"&gt;// Acknowledge receipt of the webhook with a 200 OK&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook received&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server is listening on http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run your server with &lt;code&gt;node server.js&lt;/code&gt;. It's now listening for requests on your local port 3000.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Install and Run Tunnelmole
&lt;/h4&gt;

&lt;p&gt;Next, let's get a public URL. The easiest way to install Tunnelmole is using the script for Linux/Mac or by downloading the binary for Windows.&lt;/p&gt;

&lt;p&gt;For Linux, Mac, and Windows Subsystem for Linux (WSL):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://install.tunnelmole.com/xD345/install &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;bash &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, if you have Node.js installed, you can use npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; tunnelmole
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With your local server running on port 3000, open a &lt;strong&gt;new terminal window&lt;/strong&gt; and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmole 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tunnelmole will start and display your public URLs. Copy the HTTPS URL—it will look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your Tunnelmole Public URLs are below and are accessible internet wide. Always use HTTPs for the best security

https://cqcu2t-ip-49-185-26-79.tunnelmole.net ⟶ http://localhost:3000
http://cqcu2t-ip-49-185-26-79.tunnelmole.net ⟶ http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This public URL is now forwarding all traffic directly to your local server on port 3000.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Create the Webhook in Shopify
&lt;/h4&gt;

&lt;p&gt;Now, let's tell Shopify where to send its webhook notifications.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Log in to your Shopify development store's admin panel.&lt;/li&gt;
&lt;li&gt; Navigate to &lt;strong&gt;Settings&lt;/strong&gt; in the bottom-left corner, then click on &lt;strong&gt;Notifications&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Scroll all the way down to the &lt;strong&gt;Webhooks&lt;/strong&gt; section and click &lt;strong&gt;Create webhook&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Fill out the form:

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Event:&lt;/strong&gt; Select the event you want to subscribe to. Let's use &lt;code&gt;Order creation&lt;/code&gt; for this example.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Format:&lt;/strong&gt; Choose &lt;code&gt;JSON&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;URL:&lt;/strong&gt; Paste your public Tunnelmole URL, followed by the path to your webhook handler. For our example, this would be: &lt;code&gt;https://cqcu2t-ip-49-185-26-79.tunnelmole.net/webhooks/orders&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Webhook API version:&lt;/strong&gt; Select the latest stable version.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; Click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your webhook is now active.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 4: Trigger a Test Webhook
&lt;/h4&gt;

&lt;p&gt;You can now trigger a test event in two ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;From the Shopify Admin:&lt;/strong&gt; On the Webhooks page, you'll see your newly created webhook. Click the &lt;strong&gt;Send test notification&lt;/strong&gt; button next to it.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Perform the Action:&lt;/strong&gt; Alternatively, you can perform the actual action in your store. Since we subscribed to &lt;code&gt;Order creation&lt;/code&gt;, just create a draft order and mark it as paid.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Go back to the terminal where your Node.js server is running. You should see the webhook payload printed to the console!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🎉 Received a new webhook!
{
  "id": 1234567890123,
  "name": "#1001",
  // ... and the rest of the order data
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You have successfully received a live Shopify webhook on your local machine. You can now set a breakpoint in your &lt;code&gt;/webhooks/orders&lt;/code&gt; handler and use a debugger to inspect the request in detail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Securing Your Shopify Webhook: Signature Verification
&lt;/h3&gt;

&lt;p&gt;Receiving webhooks is great, but in a production environment, you must verify that they are actually coming from Shopify. Otherwise, anyone who discovers your webhook URL could send you fake data.&lt;/p&gt;

&lt;p&gt;Shopify solves this by signing each webhook request with a &lt;strong&gt;secret key&lt;/strong&gt;. When you create a webhook, Shopify generates a secret that is shared only between your app and Shopify. Each request includes a header, &lt;code&gt;X-Shopify-Hmac-Sha256&lt;/code&gt;, containing a cryptographic signature.&lt;/p&gt;

&lt;p&gt;Your job is to calculate the same signature on your end and ensure it matches the one Shopify sent.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Update Your Server for HMAC Verification
&lt;/h4&gt;

&lt;p&gt;First, you need the &lt;strong&gt;raw request body&lt;/strong&gt; to generate the signature. The &lt;code&gt;express.json()&lt;/code&gt; middleware modifies the body, so we need to use &lt;code&gt;express.raw&lt;/code&gt; instead for our webhook endpoint. You'll also need Node's built-in &lt;code&gt;crypto&lt;/code&gt; module.&lt;/p&gt;

&lt;p&gt;Update your &lt;code&gt;server.js&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// IMPORTANT: Your Shopify Webhook Secret Key&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SHOPIFY_WEBHOOK_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your_shared_secret_from_shopify_dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Middleware to verify the webhook signature&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verifyShopifyWebhook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hmacHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Shopify-Hmac-Sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hmacHeader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook missing X-Shopify-Hmac-Sha256 header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook verification failed: Missing HMAC header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SHOPIFY_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Use crypto.timingSafeEqual to prevent timing attacks&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hmacBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hmacHeader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hashBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hmacBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hashBuffer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook verification failed: HMAC mismatch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook verification failed: Signature mismatch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;✅ Webhook signature verified successfully&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;


&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Shopify Webhook Handler is running!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Apply the raw body parser and our verification middleware ONLY to the webhook route&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhooks/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;verifyShopifyWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// If verification passes, parse the JSON body and continue&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🎉 Received a new verified webhook!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook received and verified&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server is listening on http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 2: Get Your Webhook Secret Key
&lt;/h4&gt;

&lt;p&gt;Go back to your webhook's configuration page in the Shopify admin (&lt;strong&gt;Settings &amp;gt; Notifications &amp;gt; Webhooks&lt;/strong&gt;). You will see the shared secret displayed. Click to reveal it, copy it, and paste it into the &lt;code&gt;SHOPIFY_WEBHOOK_SECRET&lt;/code&gt; constant in your &lt;code&gt;server.js&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;Restart your Node server and send another test notification. This time, you should see the "Webhook signature verified successfully" message in your console, confirming that your security check is working correctly. If you try to send a request with an invalid signature (e.g., from Postman without the correct HMAC header), the server will correctly reject it with a &lt;code&gt;401 Unauthorized&lt;/code&gt; error.&lt;/p&gt;

&lt;h3&gt;
  
  
  Production Best Practices
&lt;/h3&gt;

&lt;p&gt;With your handler working and secured, consider these best practices for a production-grade system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Asynchronous Processing:&lt;/strong&gt; A webhook handler should response immediately with a &lt;code&gt;200 OK&lt;/code&gt; status to let Shopify know it received the event. Don't perform long-running tasks like calling another API or processing a large file directly in the request handler. This can lead to timeouts. Instead, use a background job queue (e.g., BullMQ, RabbitMQ) to process the data asynchronously.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Idempotency:&lt;/strong&gt; Shopify's system guarantees "at-least-once" delivery, which means in rare cases, you might receive the same webhook more than once. Design your handler to be idempotent—meaning it can safely process the same event multiple times without causing issues. A common strategy is to log the &lt;code&gt;id&lt;/code&gt; of each received event and skip any duplicates.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Error Handling and Monitoring:&lt;/strong&gt; Implement robust logging to track incoming webhooks and any errors that occur during processing. Set up monitoring and alerts to notify you if your endpoint starts failing, as Shopify will stop sending webhooks to an endpoint that fails repeatedly.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;API Versioning:&lt;/strong&gt; Webhooks are tied to a specific Shopify API version. Shopify periodically releases new versions and retires old ones. Be sure to subscribe to a stable API version and have a plan for upgrading your code before your current version is sunset.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Shopify webhooks are a powerful tool for creating dynamic, event-driven e-commerce applications. While local development has traditionally been a bottleneck, tools like the open-source &lt;strong&gt;Tunnelmole&lt;/strong&gt; completely change the game. By creating a secure, public tunnel to your local machine, you can accelerate your development workflow, debug issues in real-time, and build more reliable integrations.&lt;/p&gt;

&lt;p&gt;By following this guide, you now have a complete, secure, and efficient process for handling Shopify webhooks with Node.js. You can receive live events, verify their authenticity, and focus on building great features instead of fighting with deployment pipelines.&lt;/p&gt;

&lt;p&gt;Ready to streamline your Shopify development? &lt;strong&gt;Get started with Tunnelmole today&lt;/strong&gt; and see how easy local webhook testing can be.&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>webhook</category>
      <category>node</category>
      <category>express</category>
    </item>
    <item>
      <title>GitHub Webhook: A Complete Guide to Automation</title>
      <dc:creator>Robbie Cahill</dc:creator>
      <pubDate>Sun, 31 Aug 2025 01:23:34 +0000</pubDate>
      <link>https://dev.to/robbiecahill/github-webhook-a-complete-guide-to-automation-3dg7</link>
      <guid>https://dev.to/robbiecahill/github-webhook-a-complete-guide-to-automation-3dg7</guid>
      <description>&lt;p&gt;In the world of modern software development, automation is the key to efficiency, consistency, and speed. One of the most powerful tools for automation within the developer ecosystem is the &lt;strong&gt;GitHub webhook&lt;/strong&gt;. Whether you're looking to build a CI/CD pipeline, send notifications to Slack, or trigger custom deployment scripts, understanding GitHub webhooks is a fundamental skill.&lt;/p&gt;

&lt;p&gt;This comprehensive guide will walk you through everything you need to know about GitHub webhooks. We'll start with the basics, build a real-world example using Node.js, and show you how to securely test your integration locally using open-source tools. By the end, you'll be able to confidently integrate GitHub webhooks into your own projects to automate your workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Exactly is a Webhook?
&lt;/h2&gt;

&lt;p&gt;Before diving into the specifics of GitHub, it's crucial to understand the core concept of a webhook.&lt;/p&gt;

&lt;p&gt;Traditionally, if you wanted to know if a status changed in another system (like a new sale on your e-commerce store), you would have to constantly ask it, "Is there anything new? Is there anything new? Is there anything new?" This process is called &lt;strong&gt;polling&lt;/strong&gt;. It's inefficient, resource-intensive, and there's always a delay between the event happening and your system finding out about it.&lt;/p&gt;

&lt;p&gt;Webhooks flip this model on its head. Instead of you asking for new information, the service tells you about it the moment it happens. This is often described as a "reverse API."&lt;/p&gt;

&lt;p&gt;Here’s a simple breakdown:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; You provide a URL (your webhook endpoint) to a service (the webhook provider).&lt;/li&gt;
&lt;li&gt; You tell the provider which events you're interested in (e.g., a "new purchase").&lt;/li&gt;
&lt;li&gt; When that event occurs, the provider immediately sends an HTTP request (usually a &lt;code&gt;POST&lt;/code&gt; request) to your URL with a payload of data describing the event.&lt;/li&gt;
&lt;li&gt; Your application, which is listening at that URL, receives the data and can take immediate action.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This event-driven approach is far more efficient and enables real-time integrations between different services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the GitHub Webhook Ecosystem
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;GitHub webhook&lt;/strong&gt; works on the exact principles described above. You can configure your GitHub repository to send a webhook payload to a specified URL whenever a certain event occurs. The range of events GitHub supports is vast, making it an incredibly powerful tool for automation.&lt;/p&gt;

&lt;p&gt;Some of the most common events you can subscribe to include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;push&lt;/code&gt;&lt;/strong&gt;: Triggered whenever a commit is pushed to a branch. This is the cornerstone of most CI/CD pipelines.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;pull_request&lt;/code&gt;&lt;/strong&gt;: Triggered when a pull request is opened, closed, reopened, or synchronized. Essential for code review automation.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;issues&lt;/code&gt;&lt;/strong&gt;: Triggered when an issue is opened, edited, closed, or labeled. Useful for project management integrations.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;release&lt;/code&gt;&lt;/strong&gt;: Triggered when a new release is published. Perfect for automating deployment notifications or package publishing.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;fork&lt;/code&gt;&lt;/strong&gt;: Triggered when a repository is forked.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;workflow_run&lt;/code&gt;&lt;/strong&gt;: Triggered when a GitHub Actions workflow is requested or completed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each event sends a unique &lt;strong&gt;payload&lt;/strong&gt;—a JSON body containing detailed information about the event. For example, a &lt;code&gt;push&lt;/code&gt; event payload includes the repository name, the branch that was pushed to, the commit hashes, the author's details, and more. Your application can parse this JSON to perform context-aware actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Our First GitHub Webhook Receiver
&lt;/h2&gt;

&lt;p&gt;The best way to learn is by doing. We're going to build a simple webhook receiver that listens for &lt;code&gt;push&lt;/code&gt; events from a GitHub repository. When a push occurs, our application will log the committer's name and the commit message to the console.&lt;/p&gt;

&lt;p&gt;To do this, we need three key components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; A web server running locally to act as our webhook endpoint.&lt;/li&gt;
&lt;li&gt; A way to give our local server a public URL that GitHub can reach.&lt;/li&gt;
&lt;li&gt; A webhook configured in our GitHub repository to point to that public URL.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Node.js and npm&lt;/strong&gt;: Make sure you have a recent version of Node.js installed. You can get it from &lt;a href="https://nodejs.org/" rel="noopener noreferrer"&gt;nodejs.org&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;A GitHub Account&lt;/strong&gt;: You'll need an account and a repository to test with. If you don't have one, create a new demo repository.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;A Text Editor&lt;/strong&gt;: Visual Studio Code or any other editor of your choice.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1: Create a Local Web Server with Express.js
&lt;/h3&gt;

&lt;p&gt;We'll use Express.js, a minimal and flexible Node.js web application framework, to create our server.&lt;/p&gt;

&lt;p&gt;First, create a new project directory and initialize it with npm.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;github-webhook-receiver
&lt;span class="nb"&gt;cd &lt;/span&gt;github-webhook-receiver
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, install Express:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;express
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, create a file named &lt;code&gt;app.js&lt;/code&gt; and add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/*
 * app.js - A simple Express server to receive GitHub webhooks.
 */&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Create the Express app&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Middleware to parse JSON bodies. GitHub sends webhooks as JSON.&lt;/span&gt;
&lt;span class="c1"&gt;// We need the raw body for signature verification, so we'll use a custom parser.&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;


&lt;span class="c1"&gt;// Define the endpoint for our GitHub webhook&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/github-webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The event type is in the X-GitHub-Event header&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;githubEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-github-event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Received a webhook for the '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;githubEvent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' event`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;---&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// We're only interested in 'push' events for this demo&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;githubEvent&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;push&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Extract relevant information from the push payload&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repositoryName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commitAuthor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head_commit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commitMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head_commit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;branch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`New push to repository: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;repositoryName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Branch: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Author: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;commitAuthor&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Commit Message: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;commitMessage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Always respond with a 200 OK to let GitHub know the webhook was received successfully.&lt;/span&gt;
    &lt;span class="c1"&gt;// If GitHub doesn't receive a 2xx response, it will consider the delivery a failure&lt;/span&gt;
    &lt;span class="c1"&gt;// and may retry, leading to duplicate processing.&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook received.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// A simple root endpoint to confirm the server is running&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server is up and running. Ready to receive GitHub webhooks at /github-webhook.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Start the server&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server started on http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break down this code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  We initialize an Express app and tell it to listen on port &lt;code&gt;8080&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Crucially, we use &lt;code&gt;express.json()&lt;/code&gt; to parse incoming JSON payloads from GitHub.&lt;/li&gt;
&lt;li&gt;  We define a single endpoint: &lt;code&gt;POST /github-webhook&lt;/code&gt;. This is the URL we'll give to GitHub.&lt;/li&gt;
&lt;li&gt;  Inside the handler, we check the &lt;code&gt;x-github-event&lt;/code&gt; header to ensure it's a &lt;code&gt;push&lt;/code&gt; event.&lt;/li&gt;
&lt;li&gt;  We then parse the &lt;code&gt;req.body&lt;/code&gt; (the JSON payload) to extract and log the repository name, author, and commit message.&lt;/li&gt;
&lt;li&gt;  Finally, we send a &lt;code&gt;200 OK&lt;/code&gt; response. This is &lt;strong&gt;vital&lt;/strong&gt;. If GitHub doesn't get a &lt;code&gt;2xx&lt;/code&gt; response, it will assume the delivery failed and retry, which could cause your automation to run multiple times for a single event.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start your server by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node app.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the message: &lt;code&gt;Server started on http://localhost:8080&lt;/code&gt;. If you visit &lt;code&gt;http://localhost:8080&lt;/code&gt; in your browser, you'll see our confirmation message.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Expose Your Local Server with Tunnelmole
&lt;/h3&gt;

&lt;p&gt;Our server is running, but it's only accessible on &lt;code&gt;localhost&lt;/code&gt;. GitHub's servers on the public internet have no way to reach it. We need to create a secure tunnel from a public URL to our local machine.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;Tunnelmole&lt;/strong&gt; comes in. Tunnelmole is an open-source tool that creates a public HTTPS URL for your locally running servers. It's simple, fast, and perfect for testing webhooks.&lt;/p&gt;

&lt;p&gt;First, install Tunnelmole. The simplest way is to use the install script for Linux, Mac, or WSL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# For Linux, macOS, and WSL&lt;/span&gt;
curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://install.tunnelmole.com/xD345/install &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;bash &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Windows users, you can download the executable and add it to your PATH. Visit the &lt;a href="https://tunnelmole.com" rel="noopener noreferrer"&gt;Tunnelmole website&lt;/a&gt; for the latest installation instructions.&lt;/p&gt;

&lt;p&gt;Once installed, open a &lt;strong&gt;new terminal window&lt;/strong&gt; (leave your Node.js server running in the first one) and run the following command, telling Tunnelmole to forward traffic to your local port &lt;code&gt;8080&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmole 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tunnelmole will start and generate a unique public URL that forwards to your local server. The output will look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ tmole 8080
Your Tunnelmole Public URLs are below and are accessible internet wide. Always use HTTPs for the best security

https://k2e6yq-ip-12-34-56-78.tunnelmole.net ⟶ http://localhost:8080
http://k2e6yq-ip-12-34-56-78.tunnelmole.net ⟶ http://localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Copy the &lt;code&gt;https&lt;/code&gt; URL&lt;/strong&gt;. This is the public address for your webhook. Anyone on the internet can now send requests to this URL, and they will be securely tunneled to your Express app running on &lt;code&gt;localhost:8080&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  How Does Tunnelmole Work?
&lt;/h4&gt;

&lt;p&gt;Tunnelmole works by establishing a secure, persistent connection from the client on your machine to the Tunnelmole service in the cloud. When a request hits your public URL, the service sends it down this tunnel to your local client, which then forwards it to your local server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F435q80fnypa8ga1amyq2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F435q80fnypa8ga1amyq2.png" alt="How Tunnelmole works" width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A key advantage of Tunnelmole is that it's &lt;strong&gt;fully open source&lt;/strong&gt;, including the server component. This means you have the option to self-host the entire service for maximum privacy and control, which is a great option for corporate environments with strict data policies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Configure the Webhook in Your GitHub Repository
&lt;/h3&gt;

&lt;p&gt;Now we have all the pieces. Let's wire them together.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Go to the GitHub repository you want to use for testing.&lt;/li&gt;
&lt;li&gt; Click on the &lt;strong&gt;"Settings"&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt; In the left sidebar, click on &lt;strong&gt;"Webhooks"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Click the &lt;strong&gt;"Add webhook"&lt;/strong&gt; button in the top right.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You'll see a configuration form. Fill it out as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Payload URL&lt;/strong&gt;: Paste your HTTPS Tunnelmole URL here, and append your endpoint path &lt;code&gt;/github-webhook&lt;/code&gt;. For example: &lt;code&gt;https://k2e6yq-ip-12-34-56-78.tunnelmole.net/github-webhook&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Content type&lt;/strong&gt;: Select &lt;code&gt;application/json&lt;/code&gt;. Our Express app is configured to parse JSON.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Secret&lt;/strong&gt;: This is a critical field for security. Create a strong, random string to use as a secret and paste it here. You can use a password generator for this. We'll see how to use this secret in the next section. For now, let's say our secret is &lt;code&gt;this-is-a-very-secret-string-12345&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Which events would you like to trigger this webhook?&lt;/strong&gt;: For this demo, select "Just the push event." In a real-world scenario, you might want to select "Send me everything" or manually choose specific events.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwaj90v5iqvydz28k6ikm.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwaj90v5iqvydz28k6ikm.webp" alt="github webhook setup" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, click the &lt;strong&gt;"Add webhook"&lt;/strong&gt; button at the bottom of the form.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Test Your GitHub Webhook!
&lt;/h3&gt;

&lt;p&gt;As soon as you add the webhook, GitHub sends a special &lt;code&gt;ping&lt;/code&gt; event to your Payload URL to verify that it's reachable.&lt;/p&gt;

&lt;p&gt;Look at your running Node.js server's terminal. You should see output like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Received a webhook for the 'ping' event
---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This confirms that the entire chain is working: GitHub -&amp;gt; Tunnelmole -&amp;gt; Your Local Express App.&lt;/p&gt;

&lt;p&gt;Now for the real test. Make a change in your local repository, commit it, and push it to GitHub.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Make a change, for example, edit your README.md&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Testing my new webhook"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; README.md

&lt;span class="c"&gt;# Commit and push&lt;/span&gt;
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: Implement amazing new feature"&lt;/span&gt;
git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As soon as the push is complete, check your server's terminal again. This time, you'll see the detailed output from our &lt;code&gt;push&lt;/code&gt; event handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Received a webhook for the 'push' event
---
New push to repository: your-username/github-webhook-receiver
Branch: main
Author: Your Name
Commit Message: "feat: Implement amazing new feature"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Success! You have successfully created, configured, and tested a &lt;code&gt;github webhook&lt;/code&gt; that communicates with a server on your local machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Securing Your Webhook Endpoint
&lt;/h2&gt;

&lt;p&gt;Our current setup works, but it has a major security flaw. The Tunnelmole URL is public. Anyone who finds it could send fake &lt;code&gt;POST&lt;/code&gt; requests to our endpoint, pretending to be GitHub and potentially triggering our automation with malicious data.&lt;/p&gt;

&lt;p&gt;This is why GitHub has the &lt;strong&gt;"Secret"&lt;/strong&gt; field. When you set a secret, GitHub uses it to create a hash-based message authentication code (HMAC) signature for each webhook payload. This signature is sent with the request in the &lt;code&gt;X-Hub-Signature-256&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;Your application can then compute its own signature using the same secret and compare it with the one sent by GitHub. If they match, you can be 100% certain the request is authentic and came from GitHub.&lt;/p&gt;

&lt;p&gt;Let's update our &lt;code&gt;app.js&lt;/code&gt; to implement this verification.&lt;/p&gt;

&lt;p&gt;First, install the built-in &lt;code&gt;crypto&lt;/code&gt; module (it comes with Node.js, so no &lt;code&gt;npm install&lt;/code&gt; is needed). We'll also use an environment variable to store our secret securely instead of hardcoding it.&lt;/p&gt;

&lt;p&gt;Update &lt;code&gt;app.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/*
 * app.js - A SECURE Express server to receive GitHub webhooks.
 */&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Built-in Node.js module&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Get the webhook secret from an environment variable for security&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GITHUB_WEBHOOK_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error: GITHUB_WEBHOOK_SECRET environment variable not set.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Middleware to parse JSON bodies.&lt;/span&gt;
&lt;span class="c1"&gt;// IMPORTANT: We need the raw request body (a buffer) to verify the signature.&lt;/span&gt;
&lt;span class="c1"&gt;// The `verify` function lets us capture it.&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// A middleware function to verify the GitHub signature&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyGitHubSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signatureHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-hub-signature-256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;signatureHeader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized: No signature provided.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Create our own signature using the secret and the raw request body&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hmac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GITHUB_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expectedSignature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`sha256=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Received Signature: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;signatureHeader&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Expected Signature: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;expectedSignature&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Use a timing-safe comparison to prevent timing attacks&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isSignatureValid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signatureHeader&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expectedSignature&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isSignatureValid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized: Invalid signature.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// If the signature is valid, proceed to the next middleware/handler&lt;/span&gt;
    &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="c1"&gt;// Apply the verification middleware ONLY to our webhook endpoint&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/github-webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;verifyGitHubSignature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;githubEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-github-event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Received a verified webhook for the '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;githubEvent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' event`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;---&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;githubEvent&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;push&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repositoryName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commitAuthor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head_commit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commitMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head_commit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;branch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`New push to repository: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;repositoryName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Branch: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Author: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;commitAuthor&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Commit Message: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;commitMessage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook received and verified.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server is up and running. Ready to receive GitHub webhooks at /github-webhook.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server started on http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; We read the secret from &lt;code&gt;process.env.GITHUB_WEBHOOK_SECRET&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; We created a middleware function &lt;code&gt;verifyGitHubSignature&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Inside the middleware, we compute our own HMAC SHA256 signature from the raw request body buffer (&lt;code&gt;req.rawBody&lt;/code&gt;) that we cleverly saved earlier.&lt;/li&gt;
&lt;li&gt; We use &lt;code&gt;crypto.timingSafeEqual&lt;/code&gt; to compare our signature with the one from the header. This is important to prevent timing attacks.&lt;/li&gt;
&lt;li&gt; If the signatures don't match, we immediately send a &lt;code&gt;401 Unauthorized&lt;/code&gt; and stop processing.&lt;/li&gt;
&lt;li&gt; We apply this middleware specifically to our &lt;code&gt;/github-webhook&lt;/code&gt; route.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, restart your server with the secret set as an environment variable. &lt;strong&gt;Replace the secret with the one you configured in the GitHub UI.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On Linux/macOS&lt;/span&gt;
&lt;span class="nv"&gt;GITHUB_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"this-is-a-very-secret-string-12345"&lt;/span&gt; node app.js

&lt;span class="c"&gt;# On Windows (Command Prompt)&lt;/span&gt;
&lt;span class="nb"&gt;set &lt;/span&gt;&lt;span class="nv"&gt;GITHUB_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"this-is-a-very-secret-string-12345"&lt;/span&gt;
node app.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To test this, go back to your webhook settings in GitHub, click "Edit", and go to the "Recent Deliveries" tab at the bottom. Click the three dots next to the last delivery and select "Redeliver." GitHub will send the same payload again. This time, your secure server will verify the signature before processing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;GitHub webhook&lt;/strong&gt; is a gateway to powerful automation. By understanding how to create, secure, and test webhook endpoints, you can build seamless CI/CD pipelines, integrate with project management tools, create custom notifications, and much more.&lt;/p&gt;

&lt;p&gt;In this guide, we've walked through the entire process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; We learned the fundamentals of webhooks.&lt;/li&gt;
&lt;li&gt; We built a Node.js and Express server to act as a webhook receiver.&lt;/li&gt;
&lt;li&gt; We used the open-source tool &lt;strong&gt;Tunnelmole&lt;/strong&gt; to expose our local server to the internet for easy and fast testing.&lt;/li&gt;
&lt;li&gt; We configured a &lt;code&gt;github webhook&lt;/code&gt; in our repository to send &lt;code&gt;push&lt;/code&gt; events to our server.&lt;/li&gt;
&lt;li&gt; Most importantly, we secured our endpoint by verifying the HMAC signature, ensuring that we only process legitimate requests from GitHub.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The combination of a flexible local server and a secure tunneling tool like Tunnelmole provides the perfect development environment for building and debugging any webhook integration. You can now apply these principles to any webhook provider, not just GitHub, to build robust, event-driven applications.&lt;/p&gt;

</description>
      <category>github</category>
      <category>webhook</category>
      <category>automation</category>
      <category>node</category>
    </item>
    <item>
      <title>How to Test WhatsApp Webhooks Locally in 5 Minutes</title>
      <dc:creator>Robbie Cahill</dc:creator>
      <pubDate>Sun, 31 Aug 2025 01:22:13 +0000</pubDate>
      <link>https://dev.to/robbiecahill/how-to-test-whatsapp-webhooks-locally-in-5-minutes-33ad</link>
      <guid>https://dev.to/robbiecahill/how-to-test-whatsapp-webhooks-locally-in-5-minutes-33ad</guid>
      <description>&lt;p&gt;Developing applications that integrate with the WhatsApp Business Platform offers incredible opportunities to engage with users. A core component of this integration is webhooks, which allow WhatsApp to send you real-time notifications about events, such as incoming messages. However, there's a significant hurdle for developers: WhatsApp requires a public HTTPS URL to send these notifications, but your development environment runs on &lt;code&gt;localhost&lt;/code&gt;, which isn't accessible from the public internet.&lt;/p&gt;

&lt;p&gt;This guide solves that exact problem. You'll learn how to securely expose your local development server to the internet to receive WhatsApp webhooks, enabling you to build and test your integration in a fast, efficient feedback loop. We'll use a simple Node.js and Express application as our webhook receiver and an open-source tool called Tunnelmole to create the necessary public URL.&lt;/p&gt;

&lt;p&gt;By the end of this comprehensive guide, you will be able to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understand the role and importance of WhatsApp webhooks.&lt;/li&gt;
&lt;li&gt;Build a basic webhook handler in Express.js to process WhatsApp notifications.&lt;/li&gt;
&lt;li&gt;Install and use Tunnelmole to get a secure, public HTTPS URL for your local server.&lt;/li&gt;
&lt;li&gt;Configure your Meta Developer App to send webhooks to your local environment.&lt;/li&gt;
&lt;li&gt;Test and debug your WhatsApp integration in real-time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What are WhatsApp Webhooks?
&lt;/h2&gt;

&lt;p&gt;WhatsApp Webhooks are automated messages sent from an application (in this case, WhatsApp) to your server when a specific event occurs. They are the backbone of building interactive and responsive applications on the WhatsApp Business Platform. Instead of you repeatedly asking the WhatsApp API if there's a new message (a process called polling), WhatsApp proactively tells you by sending an HTTP request to a "callback URL" that you provide.&lt;/p&gt;

&lt;p&gt;These webhooks can notify you of various events, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;New Messages&lt;/strong&gt;: When a user sends a message to your business number (text, images, audio, etc.).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Message Status Changes&lt;/strong&gt;: When a message you sent is delivered, read, or fails.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Account Updates&lt;/strong&gt;: Changes to your WhatsApp Business Account status.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;User Interactions&lt;/strong&gt;: When a user clicks a button in one of your interactive messages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To use webhooks, you need to set up a WhatsApp Business App through the Meta for Developers platform. During setup, WhatsApp will ask for a &lt;strong&gt;Callback URL&lt;/strong&gt;. This URL must be a public, HTTPS-secured endpoint that WhatsApp's servers can reach. This is where the challenge of local development arises, and where a tunneling tool becomes essential.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why You Need a Tunnel for Local Development
&lt;/h2&gt;

&lt;p&gt;When you run a web server on your local machine, it typically listens on an address like &lt;code&gt;http://localhost:3000&lt;/code&gt; or &lt;code&gt;http://127.0.0.1:3000&lt;/code&gt;. This address is only accessible from your own computer. For security reasons, your router and operating system firewall block incoming requests from the public internet to your &lt;code&gt;localhost&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, for a service like WhatsApp to send you a webhook, it needs to be able to make a &lt;code&gt;POST&lt;/code&gt; request to your server from its own infrastructure out on the internet. It has no way of reaching &lt;code&gt;localhost:3000&lt;/code&gt; on your machine directly.&lt;/p&gt;

&lt;p&gt;This is where a tunneling service comes in. A tunnel creates a secure connection from your local machine to a public server. It provides you with a publicly accessible URL (e.g., &lt;code&gt;https://random-subdomain.tunnelmole.net&lt;/code&gt;) that forwards all incoming traffic through the secure tunnel to your local server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F435q80fnypa8ga1amyq2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F435q80fnypa8ga1amyq2.png" alt="How Tunnelmole works" width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using a tunnel allows you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Receive Real Webhooks&lt;/strong&gt;: Test your application with actual, live data from WhatsApp.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Avoid Mocking&lt;/strong&gt;: You don't need to create fake webhook payloads or stub requests, which can be inaccurate and time-consuming.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Debug in Real-Time&lt;/strong&gt;: Set breakpoints in your code and inspect the live request from WhatsApp as it hits your local machine.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Accelerate Development&lt;/strong&gt;: Get instant feedback on code changes without deploying to a staging or production server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introducing Tunnelmole: Your Open Source Tunneling Solution
&lt;/h2&gt;

&lt;p&gt;Tunnelmole is a simple, fast, and open-source tool that gives your locally running servers a public URL. It’s designed to be straightforward, allowing you to get a tunnel up and running with a single command.&lt;/p&gt;

&lt;p&gt;Key features of Tunnelmole that make it ideal for this task include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Open Source&lt;/strong&gt;: The code for both the Tunnelmole client and server is publicly available on GitHub. This transparency allows you to inspect the code and understand exactly how it works.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Free to Use&lt;/strong&gt;: Tunnelmole's hosted service provides free public URLs that are perfect for development and testing.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Optionally Self-Hostable&lt;/strong&gt;: For advanced use cases, enhanced security, or custom domain needs, you can host the Tunnelmole service on your own infrastructure. This gives you complete control over your tunneling environment.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Easy Installation&lt;/strong&gt;: It's a native NodeJS application that can be installed via a simple script or NPM, with no complex dependencies.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step-by-Step Guide: Testing WhatsApp Webhooks with Tunnelmole
&lt;/h2&gt;

&lt;p&gt;Let's walk through the entire process from creating a local webhook handler to receiving a live notification from WhatsApp.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Node.js and npm&lt;/strong&gt;: Ensure you have Node.js (version 16.10 or later) and npm installed.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;WhatsApp Business Account&lt;/strong&gt;: You need access to the WhatsApp Business Platform. You can set this up through the &lt;a href="https://developers.facebook.com/" rel="noopener noreferrer"&gt;Meta for Developers&lt;/a&gt; portal.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;A Test Phone Number&lt;/strong&gt;: You'll need a personal WhatsApp number to send a message to your business test number.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1: Create a Simple Express.js Webhook Receiver
&lt;/h3&gt;

&lt;p&gt;First, let's create a small Node.js server using the Express framework. This server will have two endpoints required by WhatsApp:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; A &lt;code&gt;GET&lt;/code&gt; endpoint for the initial verification handshake.&lt;/li&gt;
&lt;li&gt; A &lt;code&gt;POST&lt;/code&gt; endpoint to receive the actual webhook data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Create a new project folder, &lt;code&gt;cd&lt;/code&gt; into it, and initialize a Node.js project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;whatsapp-webhook-test
&lt;span class="nb"&gt;cd &lt;/span&gt;whatsapp-webhook-test
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;express body-parser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, create a file named &lt;code&gt;index.js&lt;/code&gt; and add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/*
  A simple Express.js server to handle WhatsApp webhook verification and notifications.
*/&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bodyParser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body-parser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bodyParser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// This is the verification token you'll set in the Meta Developer App dashboard.&lt;/span&gt;
&lt;span class="c1"&gt;// It's a secret that you and WhatsApp both know.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VERIFY_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_SECRET_VERIFICATION_TOKEN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Endpoint for WhatsApp to verify your webhook&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Received a verification request.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Parse the query params&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hub.mode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hub.verify_token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;challenge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hub.challenge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c1"&gt;// Checks if a token and mode is in the query string of the request&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Checks the mode and token sent are correct&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscribe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;VERIFY_TOKEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Responds with the challenge token from the request&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WEBHOOK_VERIFIED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Responds with '403 Forbidden' if verify tokens do not match&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Verification failed. Tokens do not match.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Bad Request if mode or token is missing&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Endpoint for receiving webhook notifications&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Received a webhook notification.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Log the entire payload to inspect it&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// You can now process the webhook payload.&lt;/span&gt;
    &lt;span class="c1"&gt;// For example, check if it's a message notification:&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;whatsapp_business_account&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;change&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;messages&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
                    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`New message from &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="c1"&gt;// Here you would add your logic to reply or process the message.&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Respond with a 200 OK to acknowledge receipt of the event&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Webhook server is listening on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember to replace &lt;code&gt;"YOUR_SECRET_VERIFICATION_TOKEN"&lt;/code&gt; with a unique, secret string of your own.&lt;/p&gt;

&lt;p&gt;Run your server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your server is now running locally and listening on port 3000.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Install and Run Tunnelmole
&lt;/h3&gt;

&lt;p&gt;With your local server running, the next step is to give it a public URL.&lt;/p&gt;

&lt;p&gt;Install Tunnelmole by running the following command in your terminal. This script will detect your OS and install the correct version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://install.tunnelmole.com/xD345/install &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;bash &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Windows, you can download the executable directly and place it in your PATH.&lt;/p&gt;

&lt;p&gt;Now, run Tunnelmole and point it to the port your Express app is using (3000).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmole 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tunnelmole will start and display your public URLs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ tmole 3000
Your Tunnelmole Public URLs are below and are accessible internet wide. Always use HTTPs for the best security

https://k8sctb-ip-1-2-3-4.tunnelmole.net ⟶ http://localhost:3000
http://k8sctb-ip-1-2-3-4.tunnelmole.net ⟶ http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Copy the HTTPS URL&lt;/strong&gt; (e.g., &lt;code&gt;https://k8sctb-ip-1-2-3-4.tunnelmole.net&lt;/code&gt;). This is the URL you will give to WhatsApp.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Configure Your WhatsApp Webhook
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; Go to the &lt;a href="https://developers.facebook.com/apps/" rel="noopener noreferrer"&gt;Meta for Developers&lt;/a&gt; dashboard.&lt;/li&gt;
&lt;li&gt; Select the app you created for WhatsApp Business.&lt;/li&gt;
&lt;li&gt; From the sidebar, navigate to &lt;strong&gt;WhatsApp &amp;gt; Configuration&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; In the "Webhooks" section, click &lt;strong&gt;Edit&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; A dialog box will appear. Here you need to enter the Callback URL and Verification Token.

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Callback URL&lt;/strong&gt;: Paste your Tunnelmole HTTPS URL here, and append &lt;code&gt;/webhook&lt;/code&gt; to the end (e.g., &lt;code&gt;https://k8sctb-ip-1-2-3-4.tunnelmole.net/webhook&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Verify Token&lt;/strong&gt;: Enter the exact same secret token you defined as &lt;code&gt;VERIFY_TOKEN&lt;/code&gt; in your &lt;code&gt;index.js&lt;/code&gt; file.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; Click &lt;strong&gt;Verify and save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When you click "Verify and save", WhatsApp will send a &lt;code&gt;GET&lt;/code&gt; request to your Tunnelmole URL. Tunnelmole forwards this to your local server. Your Express app will check the token, and if it matches, it will respond with the challenge code. You should see "WEBHOOK_VERIFIED" logged in your local server's console. If everything is correct, the dialog will close, and your webhook will be active.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Subscribe to Webhook Fields
&lt;/h3&gt;

&lt;p&gt;After successful verification, you need to tell WhatsApp which events you want to be notified about.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Back on the &lt;strong&gt;WhatsApp &amp;gt; Configuration&lt;/strong&gt; page, find your webhook and click &lt;strong&gt;Manage&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; This will bring up a list of all available webhook "fields."&lt;/li&gt;
&lt;li&gt; Find the &lt;code&gt;messages&lt;/code&gt; field and click &lt;strong&gt;Subscribe&lt;/strong&gt;. This tells WhatsApp to notify you about all message-related events.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 5: Test the Integration
&lt;/h3&gt;

&lt;p&gt;Now for the exciting part! Let's test the end-to-end flow.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; From your personal WhatsApp account, send a message to the test number associated with your WhatsApp Business App.&lt;/li&gt;
&lt;li&gt; Watch the console where your local Node.js server is running.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You should immediately see the full webhook payload logged to your console, similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"whatsapp_business_account"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123456789012345"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"changes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"messaging_product"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"whatsapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"display_phone_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"16505551111"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"phone_number_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"987654321098765"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"contacts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"profile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your Name"&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"wa_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"14155552671"&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"14155552671"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wamid.HBgLMTQxNTU1NTI2NzEVAgARGBJDNzI4Q0Q4MTQ0N0JBRTdGRDAA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1668631112"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello from my phone!"&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"messages"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will also see the specific log message: &lt;code&gt;New message from 14155552671: Hello from my phone!&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Congratulations! You are now receiving live WhatsApp webhooks on your local machine. You can now set breakpoints in your &lt;code&gt;index.js&lt;/code&gt; file, modify the code, and test your logic in real-time without ever needing to deploy your changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and Best Practices
&lt;/h2&gt;

&lt;p&gt;When moving to production, always validate the request signature. WhatsApp signs each webhook request with your app's secret key using HMAC-SHA256. This is sent in the &lt;code&gt;X-Hub-Signature-256&lt;/code&gt; HTTP header. Verifying this signature ensures that the request genuinely came from WhatsApp and was not tampered with. This step is crucial for securing your production webhook endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Testing webhooks is a common pain point in modern API development. For services like the WhatsApp Business Platform, a public callback URL is non-negotiable, which often forces developers into slow, cumbersome deployment cycles just to test a small change.&lt;/p&gt;

&lt;p&gt;By using Tunnelmole, you can bridge the gap between your local development environment and the public internet. This guide has shown you how to set up a Node.js server, generate a public HTTPS URL with a single command, and configure WhatsApp to send live webhook events directly to your laptop. This workflow dramatically speeds up development, improves debugging, and ultimately helps you build more reliable integrations faster.&lt;/p&gt;

&lt;p&gt;Because Tunnelmole is open source and can be self-hosted, it offers a flexible and powerful solution that scales with your needs, from a quick test to a permanent development tool.&lt;/p&gt;

&lt;p&gt;Download Tunnelmole and start building your next WhatsApp integration today with a faster, more efficient development loop.&lt;/p&gt;

</description>
      <category>whatsapp</category>
      <category>webhook</category>
      <category>node</category>
      <category>express</category>
    </item>
    <item>
      <title>WP Webhooks: The Ultimate Guide to Connecting WordPress to Anything (2025)</title>
      <dc:creator>Robbie Cahill</dc:creator>
      <pubDate>Sun, 31 Aug 2025 01:20:21 +0000</pubDate>
      <link>https://dev.to/robbiecahill/wp-webhooks-the-ultimate-guide-to-connecting-wordpress-to-anything-2025-24e6</link>
      <guid>https://dev.to/robbiecahill/wp-webhooks-the-ultimate-guide-to-connecting-wordpress-to-anything-2025-24e6</guid>
      <description>&lt;p&gt;In the modern digital ecosystem, standalone applications are a rarity. The real power comes from integration—making different systems talk to each other to automate workflows, sync data, and create seamless user experiences. For the millions of websites powered by WordPress, this integration is often handled by &lt;strong&gt;WP Webhooks&lt;/strong&gt;. They are the invisible bridges that connect your WordPress site to the vast world of APIs, SaaS products, and custom applications.&lt;/p&gt;

&lt;p&gt;However, developing and testing these crucial integrations comes with a significant challenge: how do you allow external services on the public internet to communicate with a WordPress instance running on your local machine? This is where developers often get stuck.&lt;/p&gt;

&lt;p&gt;This ultimate guide will not only teach you everything you need to know about WP Webhooks but also provide a clear, actionable solution to this local development problem using Tunnelmole, a simple and open-source tunneling tool.&lt;/p&gt;

&lt;p&gt;We will cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What webhooks are and why they are superior to traditional API polling.&lt;/li&gt;
&lt;li&gt;Practical use cases for automating your WordPress site.&lt;/li&gt;
&lt;li&gt;The step-by-step process of creating a custom webhook listener in a WordPress plugin.&lt;/li&gt;
&lt;li&gt;How to use Tunnelmole to expose your local WordPress environment to the internet for testing.&lt;/li&gt;
&lt;li&gt;Essential security practices to protect your webhook endpoints.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Are WP Webhooks and Why Should You Care?
&lt;/h2&gt;

&lt;p&gt;Think of a webhook as a "reverse API" or a doorbell for your website.&lt;/p&gt;

&lt;p&gt;With a traditional API, you are responsible for asking for new information. For example, your WordPress site might have to poll a CRM's API every five minutes to ask, "Is there a new contact? ... Is there a new contact now? ... How about now?" This is inefficient, resource-intensive, and results in delayed updates.&lt;/p&gt;

&lt;p&gt;Webhooks flip this model on its head. Instead of your site constantly asking for data, the external service automatically sends the data to your WordPress site the moment an event happens. The external service "rings the doorbell" of your website, delivering the information as a payload of data. Your website’s job is to "answer the door" by listening for this incoming request and then taking an appropriate action.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Benefits of Using Webhooks with WordPress
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Real-Time Speed&lt;/strong&gt;: Data is pushed from the source to your WordPress site the instant an event occurs. This is essential for time-sensitive workflows like inventory management, user notifications, and financial transactions.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Improved Performance and Efficiency&lt;/strong&gt;: Your server isn't burdened with running constant API polling requests. This saves CPU cycles, reduces database load, and can lead to a faster website. Webhooks are a "set it and forget it" mechanism that consumes resources only when there's new information.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Powerful Automation&lt;/strong&gt;: Webhooks are the glue for automation platforms like Zapier, IFTTT, and custom integration hubs. They enable you to chain actions across multiple systems, triggered by an event in your WordPress site or an external application.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Limitless Integration&lt;/strong&gt;: Any application that can send an HTTP request can integrate with your WordPress site through webhooks, opening up a universe of possibilities beyond the official plugins available in the WordPress repository.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Common Use Cases for WordPress Webhooks
&lt;/h2&gt;

&lt;p&gt;The possibilities are nearly endless, but here are some popular and powerful ways developers and businesses use WP Webhooks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;E-commerce (WooCommerce)&lt;/strong&gt;: Instantly update an external inventory management system when a product is sold, notify a shipping provider to prepare a delivery, or add a customer to a marketing automation sequence after their first purchase.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;User Management&lt;/strong&gt;: Sync new WordPress user registrations to a central CRM like Salesforce or HubSpot, assign roles based on data from an external system, or send a personalized welcome sequence through a service like Mailchimp.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Content Workflows&lt;/strong&gt;: Send a notification to a Slack channel when a new post is published for review, automatically share new blog posts on social media, or trigger a backup service whenever a page is updated.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Form Submissions&lt;/strong&gt;: Connect forms built with Gravity Forms, Contact Form 7, or WPForms to a Google Sheet, a project management tool like Trello, or a customer support ticketing system.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;DevOps and CI/CD&lt;/strong&gt;: Trigger a static site rebuild on a service like Netlify or Vercel whenever content is updated in the WordPress admin, clearing the cache and deploying the latest version.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Challenge: Testing Webhooks on a Local WordPress Site
&lt;/h2&gt;

&lt;p&gt;Here's the fundamental problem every WordPress developer encounters when working with webhooks. Your local development environment, whether it's powered by XAMPP, MAMP, or Docker, runs on a private address like &lt;code&gt;localhost&lt;/code&gt; or &lt;code&gt;127.0.0.1&lt;/code&gt;. This address is only accessible from your computer.&lt;/p&gt;

&lt;p&gt;Webhook providers like Stripe, GitHub, or Zapier live on the public internet. They have no way of reaching &lt;code&gt;http://localhost:8080&lt;/code&gt; on your machine. To them, it doesn't exist.&lt;/p&gt;

&lt;p&gt;So how can you test that your custom webhook handler works correctly? The traditional, painful methods include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Deploying on every change&lt;/strong&gt;: Pushing your code to a staging server every time you make a small adjustment. This is slow, tedious, and highly inefficient.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Mocking requests&lt;/strong&gt;: Manually crafting &lt;code&gt;curl&lt;/code&gt; requests or using tools like Postman to simulate the webhook. This is useful but doesn't fully replicate the behavior of the actual service, which might have specific headers or payload structures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither of these is ideal for rapid development and debugging. You need a way for the real, live webhook to reach your local machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing Tunnelmole: Your Local Dev Environment's Public URL
&lt;/h2&gt;

&lt;p&gt;This is where a tunneling service becomes an indispensable tool for the modern developer. &lt;strong&gt;Tunnelmole&lt;/strong&gt; is a lightweight, command-line tool that creates a secure connection—a tunnel—between a public, internet-accessible URL and your local web server.&lt;/p&gt;

&lt;p&gt;It's open source, easy to install, and requires just one simple command to get started. By using Tunnelmole, you can give a service like Zapier a real, public HTTPS URL that forwards all requests directly to your local WordPress instance.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to Install and Use Tunnelmole
&lt;/h3&gt;

&lt;p&gt;Installation is straightforward. For Linux, macOS, or Windows Subsystem for Linux (WSL), you can run a single command in your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://install.tunnelmole.com/xD345/install &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;bash &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script handles the detection of your OS and installs the correct binary. For Windows users, you can download the &lt;code&gt;tmole.exe&lt;/code&gt; executable and add it to your system's PATH.&lt;/p&gt;

&lt;p&gt;Once installed, using it is even simpler. If your local WordPress site is running on port &lt;code&gt;8000&lt;/code&gt;, just run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmole 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tunnelmole will spring into action and provide you with a public URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ tmole 8000
Your Tunnelmole Public URLs are below and are accessible internet wide. Always use HTTPs for the best security

https://cqcu2t-ip-49-185-26-79.tunnelmole.net ⟶ http://localhost:8000
http://cqcu2t-ip-49-185-26-79.tunnelmole.net ⟶ http://localhost:8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, you can use the &lt;code&gt;https&lt;/code&gt; URL (&lt;code&gt;https://cqcu2t-ip-49-185-26-79.tunnelmole.net&lt;/code&gt; in this example) as the webhook endpoint in any third-party service. When that service sends a webhook, it will travel through the Tunnelmole service directly to your local WordPress site.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Tunnelmole Works
&lt;/h3&gt;

&lt;p&gt;The concept behind Tunnelmole is elegant and effective. The client on your machine establishes a persistent and secure WebSocket connection to the public Tunnelmole service. When an incoming HTTP request hits your public URL, the Tunnelmole service forwards that request down the tunnel to the client, which then passes it on to your local server (e.g., your WordPress instance). The response from your local server travels back the same way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F435q80fnypa8ga1amyq2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F435q80fnypa8ga1amyq2.png" alt="How Tunnelmole works" width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Open Source Matters
&lt;/h3&gt;

&lt;p&gt;Tunnelmole is fully open source, which provides several key advantages for developers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transparency&lt;/strong&gt;: You can inspect the code for both the client and the server to understand exactly how it works and verify its security.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control&lt;/strong&gt;: While you can use the managed Tunnelmole service for free, you also have the option to self-host the server component. This gives you complete control over your tunneling infrastructure, which can be critical for organizations with strict security or privacy requirements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community&lt;/strong&gt;: Being open source fosters a community of users who can contribute to the project, report issues, and help each other.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step-by-Step: Creating a Custom WP Webhook Listener
&lt;/h2&gt;

&lt;p&gt;Now, let's get our hands dirty and build a custom WordPress plugin to listen for an incoming webhook. In this example, we'll create an endpoint that accepts a POST request with a title and content and uses that data to create a new draft post.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Set up a Basic Custom Plugin
&lt;/h3&gt;

&lt;p&gt;First, create a new folder in your &lt;code&gt;wp-content/plugins&lt;/code&gt; directory called &lt;code&gt;my-webhook-listener&lt;/code&gt;. Inside that folder, create a PHP file with the same name: &lt;code&gt;my-webhook-listener.php&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Add the standard plugin header to this file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="cd"&gt;/**
 * Plugin Name:       My Webhook Listener
 * Description:       A simple plugin to listen for incoming webhooks and create a draft post.
 * Version:           1.0
 * Author:            Your Name
 */&lt;/span&gt;

&lt;span class="c1"&gt;// If this file is called directly, abort.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'WPINC'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;die&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// All our code will go here.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Registering a Custom API Endpoint
&lt;/h3&gt;

&lt;p&gt;WordPress comes with a powerful REST API. We can extend it by registering our own custom route. This route will serve as the URL for our webhook.&lt;/p&gt;

&lt;p&gt;We'll use the &lt;code&gt;register_rest_route&lt;/code&gt; function, hooked into the &lt;code&gt;rest_api_init&lt;/code&gt; action. Add this code to your plugin file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'rest_api_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;register_rest_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'myplugin/v1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/webhook'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'methods'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'POST'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'my_webhook_callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'permission_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'__return_true'&lt;/span&gt; &lt;span class="c1"&gt;// WARNING: For testing only. Make this secure in production.&lt;/span&gt;
    &lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break this down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;myplugin/v1&lt;/code&gt;: This is the &lt;em&gt;namespace&lt;/em&gt; for our custom endpoints. It helps avoid conflicts with other plugins.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/webhook&lt;/code&gt;: This is the &lt;em&gt;endpoint&lt;/em&gt; itself. The full URL will be &lt;code&gt;https://your-site.com/wp-json/myplugin/v1/webhook&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'methods' =&amp;gt; 'POST'&lt;/code&gt;: We specify that this endpoint only accepts POST requests, which is standard for webhooks that send data.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'callback' =&amp;gt; 'my_webhook_callback'&lt;/code&gt;: This tells WordPress to execute the &lt;code&gt;my_webhook_callback&lt;/code&gt; function when a request hits this endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'permission_callback' =&amp;gt; '__return_true'&lt;/code&gt;: This makes the endpoint public and accessible to anyone. This is great for simple testing but is a &lt;strong&gt;major security risk&lt;/strong&gt; for a live site. We will discuss how to secure this later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Writing the Webhook Callback Function
&lt;/h3&gt;

&lt;p&gt;Now, we need to create the &lt;code&gt;my_webhook_callback&lt;/code&gt; function that does the actual work. This function will receive the incoming request data, sanitize it, and create a new post.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;my_webhook_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;WP_REST_Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Get the JSON payload from the request body&lt;/span&gt;
    &lt;span class="nv"&gt;$params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_json_params&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Sanitize and validate the incoming data&lt;/span&gt;
    &lt;span class="nv"&gt;$post_title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;sanitize_text_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Webhook Draft'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$post_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'content'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;sanitize_textarea_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'content'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'No content provided.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Create the post array&lt;/span&gt;
    &lt;span class="nv"&gt;$new_post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'post_title'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post_title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'post_content'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'post_status'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Create it as a draft&lt;/span&gt;
        &lt;span class="s1"&gt;'post_author'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Assign to the admin user&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Insert the post into the database&lt;/span&gt;
    &lt;span class="nv"&gt;$post_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_insert_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$new_post&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Check for errors&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;is_wp_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WP_Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'post_creation_failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Failed to create the post.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Return a success response&lt;/span&gt;
    &lt;span class="nv"&gt;$response_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'success'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Post created successfully.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'post_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post_id&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WP_REST_Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Testing the Webhook with Tunnelmole
&lt;/h3&gt;

&lt;p&gt;Now for the magic moment. Let's test our entire setup.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Activate the Plugin&lt;/strong&gt;: Go to your local WordPress admin dashboard, navigate to "Plugins", and activate your "My Webhook Listener" plugin.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Start Tunnelmole&lt;/strong&gt;: In your terminal, start Tunnelmole and point it to your local WordPress port. For example, if your site is at &lt;code&gt;http://localhost:8080&lt;/code&gt;, run &lt;code&gt;tmole 8080&lt;/code&gt;. Copy the public &lt;code&gt;https&lt;/code&gt; URL it gives you.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Send the Webhook Request&lt;/strong&gt;: Use a tool like &lt;code&gt;curl&lt;/code&gt; or Postman to send a POST request to your new public endpoint.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is the &lt;code&gt;curl&lt;/code&gt; command. Replace the placeholder URL with your actual Tunnelmole URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST YOUR_TUNNELMOLE_URL_HERE/wp-json/myplugin/v1/webhook &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "title": "New Post from Webhook",
    "content": "This content was sent automatically via a webhook!"
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Verify the Result&lt;/strong&gt;: If everything worked, you should see the JSON success response in your terminal. Now, go to your WordPress admin dashboard, click on "Posts," and you should see a new draft titled "New Post from Webhook."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Congratulations! You have successfully created and tested a custom WP Webhook on your local machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Securing Your WP Webhooks
&lt;/h2&gt;

&lt;p&gt;Remember that &lt;code&gt;permission_callback&lt;/code&gt; we set to &lt;code&gt;__return_true&lt;/code&gt;? It's time to fix that. An unprotected webhook endpoint is a gateway for spam and malicious attacks. Here are the primary methods for securing it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 1: Using a Secret Key
&lt;/h3&gt;

&lt;p&gt;The simplest approach is to require a secret key or token in the request. The webhook provider sends the secret, and your callback function checks for it.&lt;/p&gt;

&lt;p&gt;Modify your &lt;code&gt;register_rest_route&lt;/code&gt; call to use a custom permission callback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'permission_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'my_webhook_permission_callback'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then define the permission function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;my_webhook_permission_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;WP_REST_Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Define your secret key. Store this securely, e.g., in wp-config.php&lt;/span&gt;
    &lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MY_WEBHOOK_SECRET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'a-very-long-and-random-secret-string'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$auth_header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Authorization'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Check if the header matches "Bearer &amp;lt;your_secret&amp;gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$auth_header&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$auth_header&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'Bearer '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="no"&gt;MY_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WP_Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'rest_forbidden'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Invalid secret key.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the webhook provider must include an &lt;code&gt;Authorization&lt;/code&gt; header with the value &lt;code&gt;Bearer a-very-long-and-random-secret-string&lt;/code&gt; in its request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 2: HMAC Signature Verification
&lt;/h3&gt;

&lt;p&gt;A more robust method, used by services like Stripe and GitHub, is HMAC signature verification.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The provider and your plugin share a secret key.&lt;/li&gt;
&lt;li&gt;When sending a webhook, the provider creates a cryptographic signature (a hash) of the request payload using the secret key.&lt;/li&gt;
&lt;li&gt;This signature is sent as an HTTP header (e.g., &lt;code&gt;X-Hub-Signature-256&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Your callback function independently generates its own signature of the received payload using the same secret key.&lt;/li&gt;
&lt;li&gt;If your generated signature matches the one in the header, you know the request is authentic and its payload hasn't been tampered with.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is a conceptual example of the verification logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;verify_webhook_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;WP_REST_Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$expected_signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Hub-Signature-256'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$payload_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_body&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your-shared-secret'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$expected_signature&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// The signature from GitHub is prefixed with "sha256="&lt;/span&gt;
    &lt;span class="k"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$algo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;explode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$expected_signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Calculate our own hash&lt;/span&gt;
    &lt;span class="nv"&gt;$calculated_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;hash_hmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$algo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$payload_body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Compare them securely&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;hash_equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$calculated_hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Popular Plugins for WP Webhooks
&lt;/h2&gt;

&lt;p&gt;If you don't want to code a custom solution, several powerful plugins in the WordPress ecosystem can manage webhooks for you. These are excellent for site owners or developers who need a quick, UI-driven solution.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WP Webhooks&lt;/strong&gt;: A dedicated plugin that makes it easy to both send and receive webhooks. It integrates with many popular WordPress plugins and allows you to create automations directly from the admin dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uncanny Automator&lt;/strong&gt;: A comprehensive automation plugin, often described as "Zapier for WordPress." It connects various plugins and external apps using a system of triggers and actions, with webhooks being a core component.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tools are great alternatives if your needs align with their feature sets and you prefer a no-code or low-code approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;WP Webhooks are a transformative feature for any WordPress site, turning it from a simple content management system into a dynamic, interconnected hub. They are the key to unlocking powerful automations, achieving real-time data synchronization, and building modern, integrated web experiences.&lt;/p&gt;

&lt;p&gt;While developing and testing webhooks has historically been a challenge due to the local-to-public communication barrier, open-source tools like &lt;strong&gt;Tunnelmole&lt;/strong&gt; have completely streamlined the process. With a single command, you can give your local WordPress site a public URL, enabling you to receive and debug live webhook requests in real-time. This dramatically accelerates the development cycle and eliminates the friction of traditional testing methods.&lt;/p&gt;

&lt;p&gt;By combining the flexibility of the WordPress REST API with the convenience of Tunnelmole, you have everything you need to build, test, and deploy robust and secure webhook integrations. Start automating your WordPress workflows today and unlock the true potential of your website.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webhooks</category>
      <category>php</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Get a Webhook Online: A Developer's Guide</title>
      <dc:creator>Robbie Cahill</dc:creator>
      <pubDate>Sun, 31 Aug 2025 01:18:54 +0000</pubDate>
      <link>https://dev.to/robbiecahill/how-to-get-a-webhook-online-a-developers-guide-13gb</link>
      <guid>https://dev.to/robbiecahill/how-to-get-a-webhook-online-a-developers-guide-13gb</guid>
      <description>&lt;h2&gt;
  
  
  Introduction to Online Webhooks
&lt;/h2&gt;

&lt;p&gt;In modern web development, services frequently need to communicate with each other in real-time. While APIs allow your application to request data from other services, webhooks reverse this flow, enabling services to send data to your application as events happen. Whether it's a payment confirmation from Stripe, a new commit notification from GitHub, or a custom event from an IoT device, webhooks are the engine of the event-driven web.&lt;/p&gt;

&lt;p&gt;However, there's a fundamental challenge developers face when working with webhooks: the service sending the webhook needs to reach your application over the internet. This means your application must have a public, "online" URL. During development, your application is typically running on &lt;code&gt;localhost&lt;/code&gt;, a private address accessible only from your own computer.&lt;/p&gt;

&lt;p&gt;So, how do you get your &lt;code&gt;localhost&lt;/code&gt; server online to receive and test webhooks?&lt;/p&gt;

&lt;p&gt;This guide will walk you through what it means for a webhook to be "online," why &lt;code&gt;localhost&lt;/code&gt; isn't enough, and how you can use Tunnelmole, an open-source tool, to create a secure, public URL for your local development environment. By the end, you'll be able to receive, test, and debug webhooks directly on your machine, dramatically speeding up your development workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Exactly is a Webhook? The "Reverse API" Explained
&lt;/h2&gt;

&lt;p&gt;If you're new to the concept, the easiest way to understand a webhook is to think of it as a "reverse API."&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;With a standard API&lt;/strong&gt;, you (the client) initiate a request to a server to get or send data. For example, you might make a &lt;code&gt;GET&lt;/code&gt; request to &lt;code&gt;https://api.weather.com/latest&lt;/code&gt; to fetch the current weather. You are in control of when the request is made.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;With a webhook&lt;/strong&gt;, the roles are flipped. The external service (the webhook provider) initiates a request to your application's endpoint whenever a specific event occurs. You don't ask for the data; the provider pushes it to you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This push model is incredibly efficient. Your application doesn't need to constantly poll an API endpoint asking, "Has anything new happened yet?" This saves network bandwidth, reduces server load for both you and the provider, and provides data in near real-time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Use Cases for Webhooks
&lt;/h3&gt;

&lt;p&gt;You'll find webhooks used in countless scenarios across the web:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Payment Gateways (Stripe, PayPal):&lt;/strong&gt; Receive instant notifications for successful payments, failed transactions, or new subscriptions.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Version Control (GitHub, GitLab):&lt;/strong&gt; Trigger CI/CD pipelines automatically when code is pushed to a repository.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Communication Platforms (Slack, Twilio):&lt;/strong&gt; Build bots that react to messages or receive notifications about incoming calls and SMS messages.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;eCommerce (Shopify):&lt;/strong&gt; Update inventory systems or notify shipping departments when a new order is placed.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Automation Services (IFTTT, Zapier):&lt;/strong&gt; Connect different apps and services to create powerful, automated workflows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In all these cases, the provider needs a stable, public URL—a "webhook online" endpoint—to send its HTTP POST requests to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;localhost&lt;/code&gt; Problem: Why Your Dev Server Isn't Online
&lt;/h2&gt;

&lt;p&gt;When you're building a web application, you typically run it on your local machine. You might start a Node.js server that listens on &lt;code&gt;http://localhost:3000&lt;/code&gt; or a Python server on &lt;code&gt;http://127.0.0.1:8000&lt;/code&gt;. These addresses are special; they point back to your own computer. This is perfect for development because it's fast, secure, and doesn't require an internet connection.&lt;/p&gt;

&lt;p&gt;However, from the perspective of an external service like Shopify or GitHub, &lt;code&gt;localhost&lt;/code&gt; is meaningless. When Shopify tries to send a webhook to &lt;code&gt;http://localhost:3000/shopify-webhooks&lt;/code&gt;, its servers will try to connect to &lt;em&gt;their own&lt;/em&gt; &lt;code&gt;localhost&lt;/code&gt;, not yours.&lt;/p&gt;

&lt;p&gt;Your development machine is usually sitting behind multiple layers of networking, including:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Network Address Translation (NAT):&lt;/strong&gt; Your home or office router uses NAT to manage multiple devices on a private network with a single public IP address. It doesn't know which device to send an unsolicited incoming request to.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Firewalls:&lt;/strong&gt; Your operating system and network router have firewalls that are configured by default to block almost all incoming connections for security reasons.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To make your local server accessible, you would traditionally have to deploy your code to a public server or configure complex network settings like port forwarding. This is slow, tedious, and a major roadblock to efficient development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution: Get Your Webhook Online in Minutes with Tunnelmole
&lt;/h2&gt;

&lt;p&gt;This is where a tunneling tool like Tunnelmole comes in. Tunnelmole is a simple, open-source command-line tool that creates a a secure tunnel from a public URL on the internet directly to your &lt;code&gt;localhost&lt;/code&gt; server. It bridges the gap between the public internet and your private development environment, effectively putting your webhook endpoint "online."&lt;/p&gt;

&lt;p&gt;Let's walk through the process step-by-step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create a Basic Webhook Listener in Node.js
&lt;/h3&gt;

&lt;p&gt;First, you need an application to receive the webhook. Let's create a very simple one using Express.js, a popular Node.js framework. If you don't have a Node.js project, create one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;webhook-listener
&lt;span class="nb"&gt;cd &lt;/span&gt;webhook-listener
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;express
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, create a file named &lt;code&gt;index.js&lt;/code&gt; and add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Middleware to parse JSON bodies. Most webhooks send data in JSON format.&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// Define our webhook endpoint&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhook-handler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🎉 Webhook Received!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// The data sent by the provider is in the request body&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Headers:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Body:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// It's crucial to send a 200 OK response quickly&lt;/span&gt;
    &lt;span class="c1"&gt;// to let the provider know you've received the webhook.&lt;/span&gt;
    &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook received successfully.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Webhook listener started on http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code does three key things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It starts a web server on &lt;code&gt;localhost&lt;/code&gt;, port &lt;code&gt;3000&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It uses &lt;code&gt;express.json()&lt;/code&gt; middleware to automatically parse incoming JSON payloads.&lt;/li&gt;
&lt;li&gt;It defines an endpoint at &lt;code&gt;/webhook-handler&lt;/code&gt; that listens for &lt;code&gt;POST&lt;/code&gt; requests, logs the received data, and sends back a &lt;code&gt;200 OK&lt;/code&gt; status.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Run the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see the message: &lt;code&gt;Webhook listener started on http://localhost:3000&lt;/code&gt;. Your server is running, but it's not yet online.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Install Tunnelmole
&lt;/h3&gt;

&lt;p&gt;Next, install Tunnelmole. It's a native NodeJS application and can be installed via NPM or a simple script.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Linux, macOS, or Windows Subsystem for Linux (WSL):&lt;/strong&gt;&lt;br&gt;
The easiest way is to run the installation script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://install.tunnelmole.com/xD345/install &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;bash &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Using NPM (requires Node.js):&lt;/strong&gt;&lt;br&gt;
If you have Node.js installed, you can use &lt;code&gt;npm&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; tunnelmole
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For Windows:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://tunnelmole.com/downloads/tmole.exe" rel="noopener noreferrer"&gt;Download &lt;code&gt;tmole.exe&lt;/code&gt;&lt;/a&gt; and place it in a folder that's included in your system's PATH.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Launch Tunnelmole to Go Online
&lt;/h3&gt;

&lt;p&gt;With your Node.js server running in one terminal, open a new terminal and run Tunnelmole. Point it to the port your application is listening on (in our case, &lt;code&gt;3000&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmole 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tunnelmole will connect to its service and generate a public URL. The output will look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your Tunnelmole Public URLs are below and are accessible internet wide. Always use HTTPs for the best security

https://k8sjer-ip-12-34-56-78.tunnelmole.net ⟶ http://localhost:3000
http://k8sjer-ip-12-34-56-78.tunnelmole.net ⟶ http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;That's it!&lt;/strong&gt; The HTTPS URL is now your public webhook endpoint. You can now use this URL in any webhook provider's settings. For our example, the full URL would be:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://k8sjer-ip-12-34-56-78.tunnelmole.net/webhook-handler&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Any request sent to this URL will be securely tunneled to your Express app running on &lt;code&gt;localhost:3000&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Tunnelmole Makes Your Webhook Online
&lt;/h2&gt;

&lt;p&gt;Tunnelmole's architecture is designed to be simple and effective. The diagram below illustrates how it works:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F435q80fnypa8ga1amyq2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F435q80fnypa8ga1amyq2.png" alt="How Tunnelmole works" width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Client Connection:&lt;/strong&gt; The &lt;code&gt;tmole&lt;/code&gt; client on your machine establishes a persistent, outbound connection to the Tunnelmole service running on a public server. This connection is typically a WebSocket, which works even behind most firewalls and NATs because it's initiated from inside your network.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Public URL Generation:&lt;/strong&gt; The Tunnelmole service assigns a unique public URL (e.g., &lt;code&gt;https://k8sjer-....tunnelmole.net&lt;/code&gt;) and associates it with your client's connection.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Incoming Webhook:&lt;/strong&gt; When a webhook provider (like GitHub) sends a request to your public URL, the request first hits the Tunnelmole service.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Tunneling:&lt;/strong&gt; The service immediately forwards the entire HTTP request (headers, body, and all) through the established tunnel to the Tunnelmole client on your machine.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Local Forwarding:&lt;/strong&gt; The client receives the request and forwards it to your local server (e.g., &lt;code&gt;http://localhost:3000&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Response Path:&lt;/strong&gt; Your local server processes the request and sends a response (e.g., a &lt;code&gt;200 OK&lt;/code&gt;). This response travels back through the exact same path: from your server to the &lt;code&gt;tmole&lt;/code&gt; client, through the tunnel to the service, and finally back to the original webhook provider.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This entire process happens in seconds, allowing for a seamless, real-time development experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open Source and Self-Hosting: Taking Full Control
&lt;/h2&gt;

&lt;p&gt;One of the most significant advantages of Tunnelmole is that it's &lt;strong&gt;fully open source&lt;/strong&gt;. Both the client you run locally and the server that manages the tunnels are available on GitHub. This provides several key benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Transparency and Security:&lt;/strong&gt; You can audit the code yourself to understand exactly what it's doing. There are no black boxes. For teams working in high-security environments, this is a non-negotiable feature.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Customization:&lt;/strong&gt; If you need specific features or integrations, you can fork the project and modify it to suit your needs.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Self-Hosting:&lt;/strong&gt; While Tunnelmole offers a convenient hosted service, you are not locked into it. You can deploy the &lt;a href="https://github.com/robbie-cahill/tunnelmole-service/" rel="noopener noreferrer"&gt;Tunnelmole service&lt;/a&gt; on your own infrastructure (e.g., AWS, a VPS, or even a Raspberry Pi). Self-hosting gives you complete control over your data privacy, domain names, and operational stability. You can use custom subdomains without a subscription.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Ultimate Workflow: Debugging Webhooks with Breakpoints
&lt;/h2&gt;

&lt;p&gt;The ability to get a webhook online and route it to your local machine enables the most powerful debugging technique of all: &lt;strong&gt;breakpoints&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of adding endless &lt;code&gt;console.log&lt;/code&gt; statements, you can set a breakpoint in your IDE (like Visual Studio Code) right inside your webhook handler function.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Set a breakpoint on the first line inside your &lt;code&gt;/webhook-handler&lt;/code&gt; in &lt;code&gt;index.js&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Start your application in debug mode in your IDE.&lt;/li&gt;
&lt;li&gt; Trigger a webhook from your provider (e.g., by making a push in GitHub or using a tool like Postman to send a request to your Tunnelmole URL).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When the webhook is sent, the execution of your code will &lt;strong&gt;pause&lt;/strong&gt; at the breakpoint. From there, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Inspect the entire request payload:&lt;/strong&gt; View the webhook body and headers in their raw, unprocessed form.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Step through your code:&lt;/strong&gt; Execute your logic line-by-line to see how it behaves.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Check variable states:&lt;/strong&gt; Examine the values of variables at any point in the process.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Identify errors instantly:&lt;/strong&gt; See exactly where your code fails or behaves unexpectedly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This level of interactive, real-time debugging is impossible if you have to deploy your code to a remote server for every change. It transforms webhook development from a frustrating guessing game into a streamlined, efficient process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Understanding how to get a webhook "online" is a critical skill for any modern developer. While the complexities of internet networking can make it seem daunting, tools like Tunnelmole abstract away the difficulty, allowing you to focus on what matters: building and testing your application's logic.&lt;/p&gt;

&lt;p&gt;By creating a secure tunnel from a public URL to your &lt;code&gt;localhost&lt;/code&gt; server, Tunnelmole enables you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Receive and test webhooks&lt;/strong&gt; from any provider directly on your development machine.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Debug your code interactively&lt;/strong&gt; with breakpoints for a fast and efficient workflow.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Share your work&lt;/strong&gt; with colleagues or clients before its deployed.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Leverage an open-source tool&lt;/strong&gt; that you can audit, customize, and even self-host for maximum control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next time you're tasked with integrating a webhook, you'll know exactly how to bring it online to your local environment, saving you hours of time and frustration.&lt;/p&gt;

</description>
      <category>webhook</category>
      <category>node</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Notion Webhooks: A Complete Guide for Developers (2025)</title>
      <dc:creator>Robbie Cahill</dc:creator>
      <pubDate>Sun, 31 Aug 2025 01:17:16 +0000</pubDate>
      <link>https://dev.to/robbiecahill/notion-webhooks-a-complete-guide-for-developers-2025-hop</link>
      <guid>https://dev.to/robbiecahill/notion-webhooks-a-complete-guide-for-developers-2025-hop</guid>
      <description>&lt;p&gt;Notion has become a powerhouse for personal and collaborative productivity, but its true potential is unlocked when you start automating it. For developers, this often means working with webhooks. While Notion doesn't have traditional, one-click outgoing webhooks like GitHub or Slack, its powerful API allows you to create robust, webhook-like integrations.&lt;/p&gt;

&lt;p&gt;This guide will walk you through the entire process of setting up a "Notion webhook" system. You'll learn how to build a listener service with Node.js and Express, expose it to the internet for testing and development with the open source Tunnelmole tunneling tool, and create a workflow that effectively gives you real-time notifications for changes in your Notion database.&lt;/p&gt;

&lt;p&gt;By the end of this tutorial, you'll have a fully functional webhook endpoint that can receive and process data triggered by events in your Notion workspace.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are Webhooks?
&lt;/h3&gt;

&lt;p&gt;Before diving into the specifics of Notion, let's quickly recap what a webhook is.&lt;/p&gt;

&lt;p&gt;In a typical API interaction, your application (the client) sends a request to a server, and the server sends a response. You are initiating the communication.&lt;/p&gt;

&lt;p&gt;Webhooks flip this model on its head. They are automated, server-to-server messages sent when a specific event occurs. Instead of your application polling for changes, the service (the "webhook provider") sends a POST request to a public URL you provide—your "webhook listener." This is often called a "reverse API" because it's the server that initiates the request.&lt;/p&gt;

&lt;p&gt;This event-driven approach is far more efficient than constant polling, saving resources and providing near real-time data.&lt;/p&gt;

&lt;h3&gt;
  
  
  How "Notion Webhooks" Work
&lt;/h3&gt;

&lt;p&gt;As of 2025, Notion's API does not support native outgoing webhooks. You can't simply go into a Notion database, click "Add Webhook," and paste your URL.&lt;/p&gt;

&lt;p&gt;So, how do we create a "Notion webhook"? We use a simple, powerful pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Polling Service:&lt;/strong&gt; An intermediary service monitors your Notion database for changes. This can be a third-party automation platform like Zapier or Make, or a custom script running on a schedule (e.g., a cron job).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Webhook Trigger:&lt;/strong&gt; When this service detects a change (like a new page being added or a property being updated), it triggers an action.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;HTTP Request:&lt;/strong&gt; That action is to send an HTTP POST request to a public URL that you control. This is your webhook endpoint.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Local Webhook Handler:&lt;/strong&gt; Your local application receives this request and processes the payload, which contains the data from the Notion event.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To make this work during development, your local server (running on &lt;code&gt;localhost&lt;/code&gt;) needs a public URL. This is where a tunneling tool becomes essential.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;To follow this guide, you will need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  A &lt;strong&gt;Notion Account&lt;/strong&gt; and a workspace.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Node.js&lt;/strong&gt; (v16.10 or later) and &lt;strong&gt;npm&lt;/strong&gt; installed on your machine.&lt;/li&gt;
&lt;li&gt;  Basic understanding of &lt;strong&gt;JavaScript&lt;/strong&gt; and the &lt;strong&gt;Express.js&lt;/strong&gt; framework.&lt;/li&gt;
&lt;li&gt;  A code editor like &lt;strong&gt;VS Code&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1: Set Up Your Node.js Webhook Handler
&lt;/h3&gt;

&lt;p&gt;First, let's build a simple Express.js application that will act as our webhook listener. This server will have one job: to listen for incoming POST requests on a specific endpoint.&lt;/p&gt;

&lt;p&gt;Create a new directory for your project and initialize it with npm.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;notion-webhook-handler
&lt;span class="nb"&gt;cd &lt;/span&gt;notion-webhook-handler
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, install Express and &lt;code&gt;body-parser&lt;/code&gt;, which helps parse the incoming request body.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;express body-parser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, create a file named &lt;code&gt;index.js&lt;/code&gt; and add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bodyParser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body-parser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Use body-parser middleware to parse JSON request bodies&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bodyParser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// Define the webhook endpoint&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/notion-webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🎉 Webhook received!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// The data from Notion (sent by the intermediary) will be in the request body&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payload:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// Acknowledge receipt of the webhook&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook received successfully.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// A root endpoint to confirm the server is running&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Notion Webhook Handler is running!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server listening at http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code sets up a simple web server with two routes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;GET /&lt;/code&gt;: A simple health check to confirm the server is running.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;POST /notion-webhook&lt;/code&gt;: This is our main webhook endpoint. It logs the received payload to the console and sends a &lt;code&gt;200 OK&lt;/code&gt; response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run the server to make sure everything is working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;Server listening at http://localhost:3000&lt;/code&gt; in your terminal. You can open &lt;code&gt;http://localhost:3000&lt;/code&gt; in your browser to see the "Webhook Handler is running!" message.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Get a Public URL with Tunnelmole
&lt;/h3&gt;

&lt;p&gt;Your Express server is running on &lt;code&gt;localhost&lt;/code&gt;, which is only accessible from your own computer. For an external service to send it a webhook, you need a publicly accessible HTTPS URL. This is where Tunnelmole comes in.&lt;/p&gt;

&lt;p&gt;Tunnelmole is an open-source tool that creates a secure tunnel between a public URL and your local development environment. It's incredibly simple to use and you can be up and running in seconds.&lt;/p&gt;

&lt;h4&gt;
  
  
  Install Tunnelmole
&lt;/h4&gt;

&lt;p&gt;You can install Tunnelmole using &lt;code&gt;npm&lt;/code&gt; if you have Node.js installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; tunnelmole
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, for Linux, Mac, or WSL, you can use the following &lt;code&gt;curl&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://install.tunnelmole.com/xD345/install &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;bash &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Run Tunnelmole
&lt;/h4&gt;

&lt;p&gt;With your Node.js server still running, open a &lt;strong&gt;new terminal window&lt;/strong&gt; and run the following command to tunnel port 3000:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmole 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tunnelmole will start and display a public URL that now points to your local server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ tmole 3000
Your Tunnelmole Public URLs are below and are accessible internet wide. Always use HTTPs for the best security

https://k8sjer-ip-12-34-56-78.tunnelmole.net ⟶ http://localhost:3000
http://k8sjer-ip-12-34-56-78.tunnelmole.net ⟶ http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the &lt;code&gt;https://&lt;/code&gt; URL. This is your public webhook URL. Anyone on the internet can now send requests to this URL, and they will be forwarded securely to your Express app running on &lt;code&gt;localhost:3000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Keep Tunnelmole running in this terminal window.&lt;/p&gt;

&lt;h4&gt;
  
  
  How Tunnelmole Works
&lt;/h4&gt;

&lt;p&gt;The magic of Tunnelmole lies in its simplicity. It establishes a persistent WebSocket connection from your machine to the Tunnelmole cloud service. When a request hits your public URL, the service relays it through this tunnel to your local machine, which then forwards it to your application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F435q80fnypa8ga1amyq2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F435q80fnypa8ga1amyq2.png" alt="How Tunnelmole works" width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A key benefit of Tunnelmole is that it's &lt;strong&gt;open source&lt;/strong&gt;. Both the client and the server-side code are available for review. For ultimate control and privacy, you can even &lt;strong&gt;self-host&lt;/strong&gt; the Tunnelmole service on your own server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Create a Notion Integration
&lt;/h3&gt;

&lt;p&gt;To allow an application or service to access your Notion workspace, you need to create an "integration."&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Go to the Integrations Page:&lt;/strong&gt; Navigate to &lt;a href="https://www.notion.so/my-integrations" rel="noopener noreferrer"&gt;www.notion.so/my-integrations&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Create a New Integration:&lt;/strong&gt; Click the &lt;code&gt;+ New integration&lt;/code&gt; button.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Fill in the Details:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Name:&lt;/strong&gt; Give it a descriptive name, like "My Database Webhook".&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Associated workspace:&lt;/strong&gt; Select your workspace.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Capabilities:&lt;/strong&gt; For this example, "Read content" is sufficient.&lt;/li&gt;
&lt;li&gt;  Click &lt;strong&gt;Submit&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Copy the Secret Token:&lt;/strong&gt; On the next screen, you'll see your "Internal Integration Token." Click "Show" and copy this token. Treat it like a password; don't expose it in public code repositories.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 4: Create and Share a Notion Database
&lt;/h3&gt;

&lt;p&gt;Now, let's create the Notion database that our integration will monitor.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Create a new page&lt;/strong&gt; in your Notion workspace.&lt;/li&gt;
&lt;li&gt; On the new page, select &lt;code&gt;/table&lt;/code&gt; and choose &lt;strong&gt;Table - Full page&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Give your database a name, like "My Webhook-Enabled Tasks."&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Share the database with your integration:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  Click the &lt;strong&gt;•••&lt;/strong&gt; menu in the top-right corner of the database page.&lt;/li&gt;
&lt;li&gt;  Click &lt;code&gt;+ Add connections&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Search for the integration you just created (e.g., "My Database Webhook") and select it.&lt;/li&gt;
&lt;li&gt;  Confirm the connection.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 5: Configure an Automation to Trigger the Webhook
&lt;/h3&gt;

&lt;p&gt;As mentioned, we need an intermediary to watch for changes and call our webhook. For this tutorial, we'll use Zapier as an example, as it has a free tier that is perfect for this. The same principle applies to other services like Make (formerly Integromat) or Pipedream.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Sign up or log in&lt;/strong&gt; to &lt;a href="https://zapier.com/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt; Click &lt;strong&gt;Create Zap&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Set up the Trigger:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Search for and select &lt;strong&gt;Notion&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;  For the &lt;strong&gt;Event&lt;/strong&gt;, choose &lt;strong&gt;New Database Item&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;  Connect your Notion account, authorizing Zapier to use the integration you created.&lt;/li&gt;
&lt;li&gt;  Select the &lt;strong&gt;Database&lt;/strong&gt; you just created ("My Webhook-Enabled Tasks").&lt;/li&gt;
&lt;li&gt;  Click &lt;strong&gt;Test trigger&lt;/strong&gt;. Zapier will try to find a recent item in your database. Go ahead and add a new row to your Notion table so Zapier has something to find.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Set up the Action:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  After the trigger test is successful, add a new action step.&lt;/li&gt;
&lt;li&gt;  Search for and select &lt;strong&gt;Webhooks by Zapier&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;  For the &lt;strong&gt;Event&lt;/strong&gt;, choose &lt;strong&gt;POST&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;  Now, configure the request:

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;URL:&lt;/strong&gt; Paste your public Tunnelmole URL here, followed by the endpoint path. For example: &lt;code&gt;https://k8sjer-ip-12-34-56-78.tunnelmole.net/notion-webhook&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Payload Type:&lt;/strong&gt; Set this to &lt;code&gt;Json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Data:&lt;/strong&gt; This is where you map the data from Notion. You can create a JSON object and pull in fields from the Notion database item. For example:

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;name&lt;/code&gt;: Map this to the &lt;code&gt;Name&lt;/code&gt; property from Notion.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;status&lt;/code&gt;: Map this to the &lt;code&gt;Status&lt;/code&gt; property.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;url&lt;/code&gt;: Map this to the &lt;code&gt;URL&lt;/code&gt; of the Notion page.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Headers:&lt;/strong&gt; Leave this blank for now, but you could add an authentication token here for security.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Test and Publish:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Click &lt;strong&gt;Test step&lt;/strong&gt;. Zapier will send a real POST request to your Tunnelmole URL.&lt;/li&gt;
&lt;li&gt;  Look at your Node.js server's terminal. You should see the "🎉 Webhook received!" message and the full JSON payload from Zapier printed to the console!
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My First Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Not started"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.notion.so/..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* If everything looks good, **Publish** your Zap.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Congratulations! You now have a live webhook integration. Every time you add a new item to your Notion database, Zapier will detect it and send its data to your local server via Tunnelmole.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Secure Your Webhook Endpoint
&lt;/h3&gt;

&lt;p&gt;Right now, your endpoint is public. Anyone who knows the URL can send data to it. In a production environment, you must verify that incoming requests are legitimate. A simple way to do this is with a shared secret token.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;In Zapier:&lt;/strong&gt; In the &lt;strong&gt;Headers&lt;/strong&gt; section of your Webhooks action, add a new header.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Key:&lt;/strong&gt; &lt;code&gt;X-Secret-Token&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Value:&lt;/strong&gt; A long, random string. You can generate one easily.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;In your Node.js app:&lt;/strong&gt; Create a middleware function to check for this header before your main handler runs.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Update your &lt;code&gt;index.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bodyParser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body-parser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// THIS IS YOUR SECRET. Store it securely, e.g., in an environment variable.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SHARED_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-super-secret-random-string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bodyParser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// Middleware to verify the secret token&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verifySecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;receivedSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Secret-Token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;receivedSecret&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;receivedSecret&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;SHARED_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Secrets match, proceed to the handler&lt;/span&gt;
        &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;⚠️ Unauthorized request: Invalid or missing secret token.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Apply the middleware ONLY to your webhook endpoint&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/notion-webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;verifySecret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🎉 Webhook received (and authenticated)!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payload:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook received successfully.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Notion Webhook Handler is running!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server listening at http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart your Node server (&lt;code&gt;Ctrl+C&lt;/code&gt; then &lt;code&gt;node index.js&lt;/code&gt;). Now, only requests from Zapier that include the correct secret token will be processed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion and Next Steps
&lt;/h3&gt;

&lt;p&gt;You've successfully built a complete, secure webhook integration for Notion from scratch. You learned how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Create a webhook listener with Node.js and Express.&lt;/li&gt;
&lt;li&gt;  Expose your local server to the internet using the open-source tool, &lt;strong&gt;Tunnelmole&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;  Configure a Notion integration and connect it to a database.&lt;/li&gt;
&lt;li&gt;  Use an automation service like Zapier to poll for changes and trigger your webhook.&lt;/li&gt;
&lt;li&gt;  Secure your endpoint with a shared secret.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This foundation opens up a world of automation possibilities. You could extend this application to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Send a Slack message when a task is marked "Done."&lt;/li&gt;
&lt;li&gt;  Create a calendar event from a new entry in a Notion "Meetings" database.&lt;/li&gt;
&lt;li&gt;  Sync contacts between Notion and a CRM.&lt;/li&gt;
&lt;li&gt;  Log changes to an external audit file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy automating!&lt;/p&gt;

</description>
      <category>notion</category>
      <category>webhooks</category>
      <category>api</category>
      <category>automation</category>
    </item>
    <item>
      <title>How to Test n8n Webhooks Locally with Tunnelmole</title>
      <dc:creator>Robbie Cahill</dc:creator>
      <pubDate>Sun, 31 Aug 2025 01:15:12 +0000</pubDate>
      <link>https://dev.to/robbiecahill/how-to-test-n8n-webhooks-locally-with-tunnelmole-4ood</link>
      <guid>https://dev.to/robbiecahill/how-to-test-n8n-webhooks-locally-with-tunnelmole-4ood</guid>
      <description>&lt;h2&gt;
  
  
  Introduction to n8n and Webhooks
&lt;/h2&gt;

&lt;p&gt;In the world of workflow automation, &lt;a href="https://n8n.io/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; has emerged as a powerful, flexible, and developer-friendly tool. It allows you to connect various applications and services to create sophisticated automated workflows with minimal code. A core component of these workflows, and indeed of modern web services, is the webhook.&lt;/p&gt;

&lt;p&gt;So, what is an &lt;strong&gt;n8n webhook&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;A webhook is essentially a way for applications to send automated messages or information to other applications. It's like an API but in reverse. Instead of your application actively polling a service for new data (making a request), the service automatically sends data to your application's unique "webhook URL" the moment an event occurs.&lt;/p&gt;

&lt;p&gt;In n8n, the "Webhook" node acts as a trigger, initiating a workflow whenever it receives an HTTP request. This is incredibly useful for a vast range of scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Receiving notifications from a payment gateway like Stripe after a successful transaction.&lt;/li&gt;
&lt;li&gt;Kicking off a customer onboarding sequence when a new user signs up in your CRM.&lt;/li&gt;
&lt;li&gt;Integrating with Git platforms like GitHub or GitLab to automate CI/CD pipelines.&lt;/li&gt;
&lt;li&gt;Connecting to IoT devices that send status updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem arises when you are developing and testing these workflows. Your n8n instance is likely running on your local machine (&lt;code&gt;localhost&lt;/code&gt;), which is not accessible from the public internet. External services like Stripe or GitHub cannot send their webhook notifications to a &lt;code&gt;localhost&lt;/code&gt; address.&lt;/p&gt;

&lt;p&gt;This guide will walk you through the solution: using an open-source tunneling tool called Tunnelmole to expose your local n8n instance to the internet with a public URL, making &lt;code&gt;n8n webhook&lt;/code&gt; testing a breeze.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge: Testing n8n Webhooks Locally
&lt;/h2&gt;

&lt;p&gt;When you're building a workflow in n8n, you need to test it thoroughly. For a webhook-triggered workflow, this means you need a way to send test requests to your n8n Webhook node.&lt;/p&gt;

&lt;p&gt;Let's imagine you've set up a new workflow. You add a Webhook node, and n8n generates two URLs for you: a Test URL and a Production URL.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fn8n-io%2Fn8n-docs%2Fmaster%2Fdocs%2Fassets%2Fnodes%2Fnode-embed-examples%2Fwebhook-node-parameters-test-url.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fn8n-io%2Fn8n-docs%2Fmaster%2Fdocs%2Fassets%2Fnodes%2Fnode-embed-examples%2Fwebhook-node-parameters-test-url.png" alt="n8n Webhook Node URLs" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These URLs will look something like this: &lt;code&gt;http://localhost:5678/webhook-test/d8f8a1b2-c3d4-e5f6-g7h8-i9j0k1l2m3n4&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;While you are on the same machine, you can use a tool like &lt;code&gt;curl&lt;/code&gt; or Postman to send a POST request to this &lt;code&gt;localhost&lt;/code&gt; URL and see your workflow execute.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"message":"hello world"}'&lt;/span&gt; http://localhost:5678/webhook-test/d8f8a1b2-c3d4-e5f6-g7h8-i9j0k1l2m3n4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is fine for initial, manual testing. But what happens when you need to integrate with a real-world, third-party service? You can't give Shopify, Stripe, or GitHub your &lt;code&gt;localhost&lt;/code&gt; URL. They need a public, internet-accessible HTTPS endpoint.&lt;/p&gt;

&lt;p&gt;This is where many developers get stuck. The traditional solutions are clunky:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Deploy every time:&lt;/strong&gt; You could deploy your n8n instance to a cloud server every time you make a small change. This is slow, inefficient, and clutters your development process.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Mocking services:&lt;/strong&gt; You could use tools to mock the webhook requests. This can be complex to set up and may not accurately replicate the behavior of the actual service, leading to bugs in production.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There must be a better way. And there is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Exposing Your Local Server with Tunnelmole
&lt;/h2&gt;

&lt;p&gt;The most efficient way to solve this problem is to give your local n8n instance a temporary public URL. This is where Tunnelmole comes in.&lt;/p&gt;

&lt;p&gt;Tunnelmole is a simple, open-source command-line tool that creates a secure tunnel between your local machine and a public server. It effectively forwards all requests from a public URL to your local n8n instance.&lt;/p&gt;

&lt;p&gt;This means you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Test end-to-end&lt;/strong&gt;: Use the actual third-party service to send webhooks directly to your local development environment.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Debug in real-time&lt;/strong&gt;: Set breakpoints in your custom n8n nodes or function nodes and inspect the live data coming from the webhook.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Get fast feedback&lt;/strong&gt;: Share your work-in-progress with colleagues or clients without deploying it.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Avoid complex configurations&lt;/strong&gt;: No need to mess with firewalls, NAT, or complex network settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of the key advantages of Tunnelmole is that it's &lt;strong&gt;open source and self-hostable&lt;/strong&gt;. While you can use the managed service for convenience, you have the freedom to inspect the code and even host the tunnel server yourself for maximum control and privacy.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Tunnelmole Works
&lt;/h3&gt;

&lt;p&gt;The concept is straightforward. The Tunnelmole client, running on your machine, establishes a persistent connection to a public Tunnelmole server. When an external service sends a request to your public Tunnelmole URL, the server forwards that request through the established tunnel to the client, which then passes it on to your local n8n instance on &lt;code&gt;localhost&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F435q80fnypa8ga1amyq2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F435q80fnypa8ga1amyq2.png" alt="How Tunnelmole works" width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This entire process happens in milliseconds, allowing for a seamless development experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step-by-Step Guide: Testing Your n8n Webhook with Tunnelmole
&lt;/h2&gt;

&lt;p&gt;Let's get our hands dirty and walk through the process from start to finish.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Set Up a Basic n8n Workflow
&lt;/h3&gt;

&lt;p&gt;First, ensure you have n8n running. You can run it via Docker, npm, or n8n Desktop. For this example, let's assume it's running and accessible at &lt;code&gt;http://localhost:5678&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Open your n8n canvas and create a new workflow.&lt;/li&gt;
&lt;li&gt; Click the &lt;code&gt;+&lt;/code&gt; button to add a new node.&lt;/li&gt;
&lt;li&gt; Search for and select the "Webhook" trigger node.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;This node will automatically be configured. In the properties panel on the right, you'll see the "Test URL" and "Production URL". For development, we'll use the &lt;strong&gt;Test URL&lt;/strong&gt;. Copy it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Your Test URL will be something like:&lt;/em&gt; &lt;code&gt;http://localhost:5678/webhook-test/your-unique-path&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Next, add another node to see the workflow in action. A "Respond to Webhook" node is perfect for this. Connect the Webhook node to the "Respond to Webhook" node.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In the "Respond to Webhook" node's properties, you can set a response body. Let's add a simple JSON response:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Webhook received!"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 2: Install Tunnelmole
&lt;/h3&gt;

&lt;p&gt;Installing Tunnelmole is a one-line command. It's a native NodeJS application, but you can also install a pre-compiled binary if you don't have Node.js installed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Linux, Mac, or Windows Subsystem for Linux (WSL):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open your terminal and run the following command. It will download an installation script and execute it, which detects your OS and installs the correct binary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://install.tunnelmole.com/xD345/install &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;bash &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For Windows:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tunnelmole.com/downloads/tmole.exe" rel="noopener noreferrer"&gt;Download &lt;code&gt;tmole.exe&lt;/code&gt;&lt;/a&gt; and place it in a directory that is part of your system's &lt;code&gt;PATH&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With NPM:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you have Node.js (version 16.10 or later), you can install it globally via npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; tunnelmole
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Get a Public URL for Your n8n Instance
&lt;/h3&gt;

&lt;p&gt;This is the magic step. Your n8n instance is running on port &lt;code&gt;5678&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Open a &lt;strong&gt;new terminal window&lt;/strong&gt; (leave n8n running in its own).&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run the following command:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmole 5678
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tunnelmole will connect to the service and generate your public URLs. The output will look like this:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ tmole 5678
Your Tunnelmole Public URLs are below and are accessible internet wide. Always use HTTPs for the best security

https://cqcu2t-ip-49-185-26-79.tunnelmole.net ⟶ http://localhost:5678
http://cqcu2t-ip-49-185-26-79.tunnelmole.net ⟶ http://localhost:5678
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You now have a public HTTPS URL! In this example, it's &lt;code&gt;https://cqcu2t-ip-49-185-26-79.tunnelmole.net&lt;/code&gt;. Yours will be different.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Trigger Your n8n Webhook from the Internet
&lt;/h3&gt;

&lt;p&gt;Now we'll combine the Tunnelmole URL and the n8n Webhook URL.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Tunnelmole URL:&lt;/strong&gt; &lt;code&gt;https://&amp;lt;your-subdomain&amp;gt;.tunnelmole.net&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;n8n Webhook Path:&lt;/strong&gt; &lt;code&gt;/webhook-test/your-unique-path&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Combine them to create your final public webhook URL: &lt;code&gt;https://&amp;lt;your-subdomain&amp;gt;.tunnelmole.net/webhook-test/your-unique-path&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now, let's trigger it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;In your n8n canvas, click the &lt;strong&gt;"Execute Workflow"&lt;/strong&gt; button. The Webhook node will now be listening for a test request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Go to your terminal (or use an API client like Postman) and use &lt;code&gt;curl&lt;/code&gt; to send a request to your new public URL. Make sure to send some JSON data with the &lt;code&gt;-d&lt;/code&gt; flag.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"customer_id": 123, "event": "new_order"}'&lt;/span&gt; https://&amp;lt;your-subdomain&amp;gt;.tunnelmole.net/webhook-test/your-unique-path
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;em&gt;Remember to replace the URL with your actual Tunnelmole URL.&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;When you send this request, &lt;code&gt;curl&lt;/code&gt; will receive the response you configured in the "Respond to Webhook" node:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Webhook received!"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Switch back to your n8n canvas. You should see that the workflow has executed successfully! Click on the Webhook node and inspect the "Output" tab. You'll see the JSON data you sent via &lt;code&gt;curl&lt;/code&gt;, neatly parsed and ready to be used in subsequent nodes.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.ytimg.com%2Fvi%2Fg9Q7kR82tX0%2Fmaxresdefault.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.ytimg.com%2Fvi%2Fg9Q7kR82tX0%2Fmaxresdefault.jpg" alt="n8n Successful Execution" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's it! You have successfully tested a local n8n webhook using a public URL provided by Tunnelmole. You can now configure any third-party service to send webhooks to this URL, and they will be received by your local n8n instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced Usage and Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Custom Subdomains
&lt;/h3&gt;

&lt;p&gt;The randomly generated URLs from Tunnelmole are great for quick tests, but sometimes you need a persistent URL that doesn't change every time you restart the tool. This is useful if you need to repeatedly configure a third-party service with your webhook URL.&lt;/p&gt;

&lt;p&gt;Tunnelmole supports custom subdomains. You can run it with the &lt;code&gt;as&lt;/code&gt; argument:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmole 5678 as my-n8n-workflow.tunnelmole.net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the hosted Tunnelmole service requires a subscription for custom subdomains. Alternatively, because Tunnelmole is open source, you can &lt;strong&gt;self-host the service&lt;/strong&gt; and use custom domains for free. You can find the server code and instructions on the &lt;a href="https://github.com/robbie-cahill/tunnelmole-service/" rel="noopener noreferrer"&gt;Tunnelmole Service GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security
&lt;/h3&gt;

&lt;p&gt;When you expose a local service to the internet, even temporarily, security is important.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use Production URLs in n8n:&lt;/strong&gt; When moving towards production, use the "Production URL" from the n8n Webhook node. These URLs remain active even when you're not manually executing the workflow in the editor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook Authentication:&lt;/strong&gt; The n8n Webhook node has built-in features for authentication. You can use Header Auth, Query Auth, or even a custom authentication method to ensure that only legitimate services can trigger your workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Testing &lt;code&gt;n8n webhook&lt;/code&gt; integrations no longer needs to be a roadblock in your development process. By combining the power of n8n's local instance with the simplicity of Tunnelmole, you can create a seamless and efficient workflow. You can now build, test, and debug complex automations that rely on external services, all from the comfort of your local machine.&lt;/p&gt;

&lt;p&gt;The key takeaways are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Webhooks are essential for real-time automation but challenging to test locally.&lt;/li&gt;
&lt;li&gt;  Tunnelmole provides a secure and easy way to give your local n8n instance a public URL.&lt;/li&gt;
&lt;li&gt;  The entire process takes just a few minutes to set up.&lt;/li&gt;
&lt;li&gt;  Being open-source and self-hostable, Tunnelmole gives you full control and flexibility over your development tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By adopting this workflow, you'll accelerate your development cycles, catch bugs earlier, and ultimately build more robust and reliable n8n automations. Happy automating!&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>webhook</category>
      <category>automation</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
