{"id":1128,"date":"2026-05-19T17:00:00","date_gmt":"2026-05-19T22:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1128"},"modified":"2026-03-07T03:34:55","modified_gmt":"2026-03-07T08:34:55","slug":"webhook-debugging","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/webhook-debugging\/","title":{"rendered":"Debugging Webhooks: Tools and Techniques"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Webhooks are straightforward in concept: an event happens, an HTTP POST gets sent to your server. In practice, they are one of the most frustrating things to debug. The request originates from someone else&#39;s infrastructure, there is no browser DevTools to inspect, and when something goes wrong the only evidence is a cryptic error in a log (or no log at all).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you have already set up a webhook endpoint (see the <a href=\"https:\/\/tolinku.com\/blog\/webhook-setup-guide\/\">webhook setup guide<\/a> if you have not), this article covers what to do when things break. We will walk through the most common failure modes, tools for local development, logging strategies that actually help, and monitoring approaches that catch problems before your users notice.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For a broader look at how webhooks fit into deep linking workflows, see the <a href=\"https:\/\/tolinku.com\/blog\/webhooks-integrations-deep-linking\/\">webhooks and integrations overview<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><img decoding=\"async\" src=\"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/platform-webhooks.png\" alt=\"Tolinku webhook configuration for event notifications\">\n<em>The webhooks page with create form, webhook list, and delivery log.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Four Categories of Webhook Failures<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Almost every webhook issue falls into one of four buckets:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Delivery failures.<\/strong> The request never reaches your server. DNS issues, firewall rules, TLS errors, or your server being down.<\/li>\n<li><strong>Signature mismatches.<\/strong> The request arrives but your verification code rejects it. Usually caused by body parsing issues or incorrect secrets.<\/li>\n<li><strong>Payload parsing errors.<\/strong> The signature checks out but your handler crashes trying to process the payload. Missing fields, unexpected types, or encoding problems.<\/li>\n<li><strong>Timeouts.<\/strong> Your server receives the request but takes too long to respond. The sending platform marks the delivery as failed and retries.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Knowing which category you are dealing with cuts your debugging time significantly. The Tolinku dashboard&#39;s <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/webhooks\/testing-and-deliveries\/\">delivery log<\/a> shows the HTTP status code and response time for every delivery attempt, which tells you immediately whether the problem is on the network side (category 1), in your application logic (categories 2-3), or a performance issue (category 4).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Setting Up Local Development<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The biggest obstacle to debugging webhooks is that you cannot receive them on <code>localhost<\/code>. The sending platform needs a publicly reachable URL. There are several tools that solve this by creating tunnels from the public internet to your local machine.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">ngrok<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/ngrok.com\/docs\" rel=\"nofollow noopener\" target=\"_blank\">ngrok<\/a> is the most widely used option. Install it, then run:<\/p>\n\n\n\n<pre><code class=\"language-bash\">ngrok http 3000\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This gives you a public URL like <code>https:\/\/abc123.ngrok-free.app<\/code> that forwards to your local port 3000. Copy that URL into the Tolinku dashboard as your webhook endpoint.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">ngrok also provides a local inspection UI at <code>http:\/\/127.0.0.1:4040<\/code> where you can see every request, replay failed deliveries, and inspect headers and bodies. This inspection interface alone makes it worth using during development.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Webhook.site<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/webhook.site\/\" rel=\"nofollow noopener\" target=\"_blank\">Webhook.site<\/a> gives you a temporary public URL that logs incoming requests. It is useful when you want to inspect raw payloads without writing any code. You can see the exact headers, body, and timing of each delivery. The limitation is that it does not forward to your local server, so it is better for inspection than for end-to-end testing.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Other Tunnel Options<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/developers.cloudflare.com\/cloudflare-one\/connections\/connect-networks\/\" rel=\"nofollow noopener\" target=\"_blank\">Cloudflare Tunnel<\/a> provides a free tunnel option that works well for longer-lived development environments. <a href=\"https:\/\/theboreddev.com\/use-localtunnel-for-testing\/\" rel=\"nofollow noopener\" target=\"_blank\">localtunnel<\/a> is an open-source alternative if you want to avoid creating accounts.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Whichever tool you use, the workflow is the same: start your local server, create a tunnel, register the tunnel URL in your Tolinku Appspace&#39;s <a href=\"https:\/\/tolinku.com\/features\/webhooks\">webhook settings<\/a>, and trigger test events.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Using Test Deliveries<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before debugging with real events, use Tolinku&#39;s test delivery feature. In the webhooks page of your Appspace dashboard, each endpoint has a &quot;Send Test&quot; option. This sends a sample payload to your endpoint and reports the result immediately.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Test deliveries are valuable for two reasons. First, they confirm basic connectivity: can the platform reach your endpoint at all? Second, they provide a known-good payload. If your handler works with a test delivery but fails on real events, the problem is in your event-specific processing logic, not in the plumbing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If a test delivery fails, check the delivery log for the HTTP status code. A <code>000<\/code> or connection error means the platform could not reach your URL. A <code>4xx<\/code> or <code>5xx<\/code> means your server received the request but responded with an error.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Debugging Signature Mismatches<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Signature verification is where most webhook debugging happens. Tolinku signs every webhook delivery using HMAC-SHA256 over the raw request body. The signature is sent in the <code>X-Webhook-Signature<\/code> header, and the event type is in the <code>X-Webhook-Event<\/code> header.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here is a correct verification implementation:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">const crypto = require(&#39;crypto&#39;);\n\nfunction verifyWebhookSignature(req, secret) {\n  const signature = req.headers[&#39;x-webhook-signature&#39;];\n\n  if (!signature) {\n    return false;\n  }\n\n  \/\/ Sign the raw request body (not parsed JSON)\n  const expected = crypto\n    .createHmac(&#39;sha256&#39;, secret)\n    .update(req.body) \/\/ req.body is a Buffer from express.raw()\n    .digest(&#39;hex&#39;);\n\n  const sig = Buffer.from(signature, &#39;hex&#39;);\n  const exp = Buffer.from(expected, &#39;hex&#39;);\n\n  if (sig.length !== exp.length) return false;\n\n  return crypto.timingSafeEqual(sig, exp\n  );\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The most common cause of signature failures is <strong>body parsing<\/strong>. If you use <code>express.json()<\/code> before your webhook route, Express parses the body into a JavaScript object, and the raw bytes are gone. The HMAC is computed over the raw request body, so you need to preserve it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The fix is to capture the raw body during parsing:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">app.use(&#39;\/webhooks&#39;, express.json({\n  verify: (req, res, buf) =&gt; {\n    req.rawBody = buf.toString(&#39;utf-8&#39;);\n  }\n}));\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Other common signature issues:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Wrong secret.<\/strong> Double-check that you are using the webhook signing secret from the Tolinku dashboard, not your API key (which starts with <code>tolk_sec_<\/code> or <code>tolk_pub_<\/code>). The signing secret is specific to each webhook endpoint.<\/li>\n<li><strong>Encoding mismatch.<\/strong> If you are comparing the signature as a base64 string but the platform sends it as hex (or vice versa), verification will always fail. Tolinku uses hex encoding.<\/li>\n<li><strong>Timestamp not included.<\/strong> The signed payload is <code>timestamp.body<\/code>, not just <code>body<\/code>. If you forget to prepend the timestamp, the HMAC will not match.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">When you are stuck, log both the expected and received signatures (in a development environment only) to see whether they are completely different (wrong secret or wrong payload) or just slightly off (encoding issue).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Debugging Payload Parsing Errors<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Once the signature is valid, the next failure point is your handler logic. These errors are usually straightforward but can be tricky to reproduce because they depend on specific event types or data shapes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A defensive handler pattern:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">app.post(&#39;\/webhooks\/tolinku&#39;, (req, res) =&gt; {\n  \/\/ Respond immediately to avoid timeout\n  res.status(200).json({ received: true });\n\n  try {\n    const { event, timestamp, data } = JSON.parse(req.body.toString());\n\n    if (!event || !data) {\n      console.error(&#39;Malformed webhook payload:&#39;, JSON.stringify(req.body));\n      return;\n    }\n\n    switch (event) {\n      case &#39;link.clicked&#39;:\n        handleLinkClicked(data);\n        break;\n      case &#39;deferred_link.claimed&#39;:\n        handleLinkOpened(data);\n        break;\n      case &#39;install.tracked&#39;:\n        handleLinkInstalled(data);\n        break;\n      case &#39;referral.created&#39;:\n        handleLinkConverted(data);\n        break;\n      case &#39;referral.completed&#39;:\n        handleLinkReferred(data);\n        break;\n      default:\n        handleCustomEvent(data);\n        break;\n      default:\n        console.warn(`Unhandled webhook event type: ${event}`);\n    }\n  } catch (err) {\n    console.error(&#39;Webhook processing error:&#39;, err);\n  }\n});\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Key patterns in this code:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Respond before processing.<\/strong> Send the <code>200<\/code> immediately, then process asynchronously. This prevents timeouts even if your handler logic is slow.<\/li>\n<li><strong>Validate the envelope.<\/strong> Check that <code>event<\/code> and <code>data<\/code> exist before switching on the event type.<\/li>\n<li><strong>Handle unknown events gracefully.<\/strong> New event types may be added in the future. Log them, but do not crash.<\/li>\n<li><strong>Catch errors.<\/strong> A thrown exception in a webhook handler is invisible to the sender. Log it so you can find it.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Common parsing problems include assuming a field exists when it is optional, treating numbers as strings (or vice versa), and hardcoding event types without a default case.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Debugging Timeouts<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Most webhook platforms expect a response within 5 to 30 seconds. If your handler does heavy processing (database writes, external API calls, image processing), you can easily exceed that window.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The solution is always the same: acknowledge receipt immediately and process the event asynchronously. In Node.js, you can use a simple queue:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">const eventQueue = [];\n\napp.post(&#39;\/webhooks\/tolinku&#39;, (req, res) =&gt; {\n  res.status(200).json({ received: true });\n  eventQueue.push(req.body);\n});\n\n\/\/ Process events in the background\nsetInterval(() =&gt; {\n  while (eventQueue.length &gt; 0) {\n    const event = eventQueue.shift();\n    processEvent(event).catch(err =&gt; {\n      console.error(&#39;Failed to process event:&#39;, err);\n    });\n  }\n}, 100);\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For production systems, replace the in-memory queue with something durable like <a href=\"https:\/\/docs.bullmq.io\/\" rel=\"nofollow noopener\" target=\"_blank\">BullMQ<\/a> (backed by Redis), a database-backed job queue, or a message broker. The point is to decouple receipt from processing so that delivery never times out.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Logging Strategies That Help<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Generic logging tells you something went wrong. Good webhook logging tells you exactly what went wrong and with which delivery.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Log these fields on every webhook receipt:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">function logWebhookReceipt(payload) {\n  console.log(JSON.stringify({\n    type: &#39;webhook_received&#39;,\n    event: payload.event,\n    event_timestamp: payload.timestamp,\n    status: &#39;received&#39;,\n    received_at: new Date().toISOString()\n  }));\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Structured JSON logs are critical here. If you are running in production with a log aggregation service (Datadog, Grafana Loki, AWS CloudWatch), structured logs let you filter by event type, search by delivery ID, and correlate webhook receipts with downstream processing errors.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Log again after processing completes (or fails):<\/p>\n\n\n\n<pre><code class=\"language-javascript\">function logWebhookProcessed(deliveryId, event, success, error) {\n  console.log(JSON.stringify({\n    type: &#39;webhook_processed&#39;,\n    delivery_id: deliveryId,\n    event: event,\n    status: success ? &#39;success&#39; : &#39;failed&#39;,\n    error: error ? error.message : null,\n    processed_at: new Date().toISOString()\n  }));\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">With both log entries, you can quickly answer questions like: &quot;Did we receive the delivery?&quot; and &quot;Did we successfully process it?&quot; These are different questions with different answers, and conflating them is a common debugging pitfall.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Monitoring and Alerting<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Logging helps you investigate problems after they occur. Monitoring helps you catch them as they happen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Track these metrics:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Delivery success rate.<\/strong> The percentage of incoming webhooks that return a <code>200<\/code> response. A sudden drop means something is broken.<\/li>\n<li><strong>Processing success rate.<\/strong> The percentage of received webhooks that your handler processes without error. This can differ from delivery success rate if you respond <code>200<\/code> before processing.<\/li>\n<li><strong>Processing latency.<\/strong> How long your handler takes to complete. A gradual increase is an early warning sign.<\/li>\n<li><strong>Queue depth.<\/strong> If you are using an async queue, monitor its size. A growing queue means you are receiving events faster than you can process them.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Set alerts on the delivery success rate. If it drops below 95% for more than a few minutes, something needs attention. The Tolinku dashboard&#39;s webhook delivery log shows failed deliveries from the platform&#39;s perspective, but your own monitoring catches failures that happen after the <code>200<\/code> response.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A Debugging Checklist<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When a webhook is not working, run through this checklist in order:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Is the endpoint URL correct?<\/strong> Check for typos, missing paths, and HTTP vs. HTTPS.<\/li>\n<li><strong>Can the platform reach your server?<\/strong> Use a test delivery from the Tolinku dashboard. Check the delivery log for the response code.<\/li>\n<li><strong>Is your server running and listening on the right port?<\/strong> If using a tunnel, verify the tunnel is active.<\/li>\n<li><strong>Is your TLS certificate valid?<\/strong> Expired certificates cause silent connection failures.<\/li>\n<li><strong>Is the raw body preserved for signature verification?<\/strong> Check your body parsing middleware.<\/li>\n<li><strong>Are you using the correct signing secret?<\/strong> Not your API key, not another endpoint&#39;s secret.<\/li>\n<li><strong>Does your handler return a response before the timeout?<\/strong> Add logging to measure response time.<\/li>\n<li><strong>Are you handling the specific event type?<\/strong> Check your switch statement or router for the event in question.<\/li>\n<li><strong>Is a firewall or WAF blocking the requests?<\/strong> Some security tools block automated POST requests that do not carry a browser user agent.<\/li>\n<li><strong>Are retries causing duplicate processing?<\/strong> Implement idempotency based on the delivery <code>id<\/code> field.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Work through this list from top to bottom. Most issues resolve within the first three or four items.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Wrapping Up<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Debugging webhooks is fundamentally different from debugging a standard API integration. You do not control the request, you cannot easily reproduce it, and failures can be silent. The combination of good local tooling (tunnels and test deliveries), structured logging, and proactive monitoring turns webhook debugging from a guessing game into a systematic process.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Start by using <a href=\"https:\/\/ngrok.com\/docs\" rel=\"nofollow noopener\" target=\"_blank\">ngrok<\/a> or <a href=\"https:\/\/webhook.site\/\" rel=\"nofollow noopener\" target=\"_blank\">Webhook.site<\/a> during development. Use Tolinku&#39;s <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/webhooks\/testing-and-deliveries\/\">test delivery feature<\/a> to verify connectivity and signature verification. Add structured logs that track both receipt and processing. And set up monitoring so you catch failures before your downstream systems go stale.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For more on setting up webhook endpoints from scratch, see the <a href=\"https:\/\/tolinku.com\/blog\/webhook-setup-guide\/\">webhook setup guide<\/a>. For a broader look at building integrations around deep link events, see the <a href=\"https:\/\/tolinku.com\/blog\/webhooks-integrations-deep-linking\/\">webhooks and integrations overview<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A practical guide to debugging webhook delivery failures, signature mismatches, payload parsing errors, and timeouts. Covers local development tunnels, logging strategies, and monitoring.<\/p>\n","protected":false},"author":2,"featured_media":1127,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Debugging Webhooks: Tools and Techniques for 2026","rank_math_description":"Practical guide to debugging webhook delivery failures, signature mismatches, payload parsing errors, and timeouts. Covers ngrok, logging, and monitoring.","rank_math_focus_keyword":"debugging webhooks","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-webhook-debugging.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-webhook-debugging.png","footnotes":""},"categories":[15],"tags":[62,74,20,264,263,274,275,80,61],"class_list":["post-1128","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-engineering","tag-api","tag-debugging","tag-deep-linking","tag-engineering","tag-integrations","tag-monitoring","tag-node-js","tag-testing","tag-webhooks"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1128","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=1128"}],"version-history":[{"count":4,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1128\/revisions"}],"predecessor-version":[{"id":2253,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1128\/revisions\/2253"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1127"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1128"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1128"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1128"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}