Your GoHighLevel workflow shows that a contact moved through the sequence, but your backend server never received the HTTP POST request. I’ve debugged this exact issue dozens of times across client setups. Here is how to isolate whether the workflow failed to trigger, the webhook action timed out, or a malformed custom field blew up your JSON payload.

The 4 Failure Modes in HighLevel Webhooks
When an outbound webhook in HighLevel doesn’t hit your endpoint, it almost always breaks at one of four specific choke points. Don’t guess—eliminate them in order:
- Trigger-level failure: The contact never met your trigger filters (e.g., missing a required tag or specific form field value), so the workflow never actually fired.
- Action-level silent branch: The workflow triggered, but an upstream “If/Else” condition or missing merge field halted execution or branched away before hitting the Webhook action.
- HTTP transport failure: HighLevel sent the request, but your server threw a non-2xx status code (like a 403 Forbidden or 500 error) or timed out past HighLevel’s execution window.
- Payload serialization crash: A custom value with unescaped double quotes or unexpected line breaks corrupted the raw JSON body, causing your server’s JSON parser to reject the request with a 400 Bad Request.
Step 1: Check Execution Logs and Enrollment History
Never rely on the “Test Action” button inside the workflow builder to verify your setup. “Test Action” sends static dummy data directly from the UI and completely bypasses your trigger filters and branch logic, masking underlying configuration bugs.
To see what actually happened to a live contact, open your workflow and click the Execution Logs tab in the top navigation bar. Filter by the contact’s email or phone number.
Check the status column next to your Webhook action:
- Waiting: The contact is paused at a prior “Wait” step or manual review step.
- Skipped: An If/Else branch directed the contact around the Webhook action block.
- Failed: HighLevel tried to send the HTTP call and received an error code or timed out. Click the failed step to inspect the exact HTTP response code and response body your server returned.
- Executed: HighLevel dispatched the request and received a
200 OKor201 Createdresponse within its timeout window.
If the contact doesn’t show up in the Execution Logs at all, check your Workflow Settings and verify that Allow Re-entry is turned on. By default, HighLevel blocks contacts from entering the same workflow more than once.
Step 2: Isolate the Endpoint with Webhook.site
If the Execution Log shows a failure—or if it claims “Executed” but nothing hits your database—you need to know if HighLevel is dropping the request or if your backend is silently ignoring it. The fastest way to test this is by swapping your target URL with a temporary Webhook.site endpoint.
Copy your unique Webhook.site URL, paste it into the HighLevel Webhook action URL field, and trigger a live test contact.
If the payload lands on Webhook.site within a second or two, your issue is 100% inside your backend infrastructure (firewalls, Cloudflare WAF challenges, or route handlers). If nothing arrives on Webhook.site, the bug is inside HighLevel’s workflow logic.
Here is what a standard, healthy HighLevel webhook POST body looks like when delivered cleanly:
{ "contact_id": "AB12cDEf34GhIjKL56Mn", "first_name": "Alex", "last_name": "Rivera", "email": "alex.rivera@example.com", "phone": "+15550192834", "tags": [ "lead-source-ad", "priority-high" ], "customData": { "account_tier": "Enterprise", "deal_value": 4500 }, "location": { "id": "loc_998877aabbcc", "name": "Main Agency Sub-Account" }
}If you’re routing location data programmatically across multiple sub-accounts, make sure you know how to find your GoHighLevel location ID for API v2 requests so you can verify that the payload belongs to the expected sub-account.
Step 3: Fix Broken Merge Fields and Payload Syntax
When you switch the webhook method from standard POST to a custom JSON payload, unescaped custom values will break the JSON parser. If a lead enters a quotation mark inside a form text field (e.g., 5'10" height), a naive custom JSON template will create invalid JSON syntax and crash the parser.
Always provide fallback values or handle missing keys. Check out our guide on GoHighLevel merge fields and fallback values to prevent null keys from stripping your data.
Here is an example of a fragile custom JSON template versus a clean, safe payload structure:
{ "lead_name": "{{contact.name}}", "notes": "{{contact.notes}}", "lead_score": {{contact.score | default: 0}}
}If contact.notes contains unescaped double quotes or multi-line text, HighLevel’s variable replacement can produce invalid JSON. Review our guide on structuring custom values and custom fields in GoHighLevel webhooks to ensure your custom templates stay RFC 8259 compliant.
Step 4: Resolve HTTP 301 Redirect Drops
A classic gotcha in HighLevel webhooks is URL redirection. If your endpoint is https://api.yourdomain.com/v1/ghl-webhook, but you enter http://api.yourdomain.com/v1/ghl-webhook (HTTP instead of HTTPS) or omit a trailing slash that your web framework expects, your server issues an HTTP 301 Moved Permanently or 302 Found.
When HTTP clients follow a 301/302 redirect, they standardly rewrite the original POST into an empty GET request. HighLevel drops the POST body entirely during this redirect hop, and your receiver gets an empty request.
Test your endpoint URL directly in your terminal using curl to inspect the response headers:
curl -I -X POST https://api.yourdomain.com/ghl-webhook -H "Content-Type: application/json" -d '{"test": true}'Check the first line of the output. If you see HTTP/1.1 301 Moved Permanently or HTTP/2 308 Permanent Redirect, update your HighLevel Webhook action URL immediately to the exact, final destination URL (including correct protocol and trailing slashes).
Step 5: Inspect Server Timeouts and Return 200 OK Immediately
HighLevel enforces an internal timeout window of around 5 to 10 seconds. If your receiving endpoint tries to process heavy logic—like generating PDFs, calling external AI APIs, or running slow SQL queries—before returning an HTTP response, HighLevel will cut the connection and mark the action as failed.
To avoid this, decouple your intake handler from your business logic. Validate the payload, drop the raw event into an asynchronous background queue (like BullMQ, Celery, or SQS), and return an immediate 200 OK response with {} in under 500ms.
Here is an Express/Node.js example that acknowledges the webhook instantly before running any long-running tasks:
const express = require('express');
const app = express(); app.use(express.json()); app.post('/ghl-webhook', (req, res) => { const payload = req.body; // Check basic payload validity if (!payload || !payload.contact_id) { return res.status(400).json({ error: 'Missing contact_id' }); } // Acknowledge HighLevel instantly to prevent timeout errors res.status(200).json({ status: 'received', timestamp: Date.now() }); // Run heavy background operations outside the HTTP cycle setImmediate(async () => { try { console.log(`Processing contact: ${payload.contact_id}`); // Custom logic, CRM syncing, or AI tasks go here } catch (err) { console.error('Background processing error:', err.message); } });
}); app.listen(3000, () => { console.log('Webhook receiver running on port 3000');
});If your account handles high-volume campaigns or bulk contact imports, read our guide on how to handle webhook spikes with a Redis buffer queue to prevent server crashes and dropped payloads.
Step 6: Handle Cloudflare and IP Whitelisting Blocks
If your server sits behind Cloudflare, AWS WAF, or an aggressive reverse proxy, requests from HighLevel’s outbound servers might trigger bot detection rules and get blocked with an HTTP 403 Forbidden or a managed CAPTCHA challenge.
To confirm whether your firewall is dropping HighLevel:
- Open your Cloudflare dashboard and go to Security > Events. Filter by the URI path of your webhook endpoint and check for blocked requests.
- Add a WAF Custom Rule in Cloudflare that sets the action to Bypass (or “Skip WAF”) for your specific webhook route (e.g.,
(http.request.uri.path eq "/ghl-webhook")). - If your infrastructure requires strict IP allowlisting, refer to the official HighLevel Help Center or the HighLevel Developer Portal for the latest outbound server IP ranges.
Step-by-Step Recovery Checklist
Before running your next batch of live leads, run through this quick checklist:
- Workflow is Published: The top-right toggle must be set to “Published”, not “Draft”.
- Re-entry Enabled: In Workflow Settings, verify “Allow Re-entry” is enabled if you’re testing with the same contact record repeatedly.
- No Filter Blocks: Ensure your test contact matches all trigger filters, tag requirements, and If/Else branches.
- Direct HTTPS URL: Confirm your webhook URL uses HTTPS and doesn’t trigger 301/302 redirects.
- Fast 200 OK Response: Make sure your backend acknowledges the request in under 500ms before processing heavy jobs.
- Valid JSON Structure: If using custom data templates, verify that unescaped quotes or missing keys won’t break the JSON envelope.
Frequently Asked Questions
Why does the “Test Action” button work, but live contacts fail?
The “Test Action” modal sends static sample data directly from the builder. It skips your workflow triggers, If/Else branching logic, and real custom fields. In production, real contact records often contain missing fields, special characters, or fail upstream filter conditions that the test button never encounters.
Can I resend failed webhooks in GoHighLevel?
HighLevel doesn’t have a single-click “Retry Webhook” button in the Execution Logs. To re-run failed contacts, apply a temporary tag like retry-webhook to them, then build a simple one-step workflow triggered by that tag containing only your Webhook action.
Does HighLevel retry webhooks automatically on HTTP 500 errors?
HighLevel will attempt automated retries for transient network drops on certain tiers, but consistent 4xx client errors or prolonged 5xx server errors are marked as failed and won’t loop indefinitely. You should make your receiver fault-tolerant and monitor Execution Logs proactively.
What is the maximum payload size for a HighLevel webhook?
Standard HighLevel contact and trigger payloads range between 2KB and 15KB. Avoid passing massive base64-encoded file strings or large text blobs through custom data fields. Send record IDs and metadata via the webhook, then fetch large files or extended history separately using the API.

