You wire up an outbound webhook in GoHighLevel, fire a test contact through your workflow, and immediately hit a wall: 401 Unauthorized or 403 Forbidden. The receiving API expects a Bearer token, an API key, or a specific Content-Type header, and GHL just dumped a raw payload with default headers. Here is how to configure custom headers in both the native webhook UI and the Custom Code action without needing Zapier or an external proxy.

The Native Webhook Action vs Custom Code Action
HighLevel gives you two ways to send outbound HTTP requests from a workflow:
- Standard Webhook Action: Sends a standard
POSTorGETrequest with the entire contact payload. GHL’s workflow builder now lets you add custom key-value header pairs right in the action interface. It is fast, clean, and handles 90% of basic API key authentications. - Custom Code Action: Runs server-side Node.js directly inside the workflow runner. You get full control over the HTTP method, custom headers, payload shape, and response parsing.
If your target endpoint accepts standard contact JSON and just needs a static API key or Bearer token header, use the native webhook action. If you need dynamic headers, HMAC signatures, custom payload formatting, or need to handle the response data downstream, use Custom Code.
Method 1: Configure Custom Headers in the Native Webhook Action
You can add static or merge-tag-based headers directly in the standard webhook action without touching code:
- Click the plus icon (+) on your workflow canvas and select Webhook.
- Set the method to POST (or GET) and paste your endpoint URL.
- Scroll down to the Headers section and click Add Item.
- Put your header name on the left (e.g.,
Authorization) and the value on the right (e.g.,Bearer your_api_key_here). - Add any extra headers your endpoint demands, like
X-Api-KeyorAccept: application/json.
To avoid hardcoding secrets across duplicate workflows or snapshots, drop your tokens into account custom values. You can reference custom values in GoHighLevel directly in the header value field using merge tags (e.g., Bearer {{custom_values.api_secret_key}}).
Method 2: Sending Advanced Headers via Custom Code (Node.js)
When an API requires dynamic headers (like an HMAC signature, timestamp header, or strict JSON formatting), the native webhook action won’t cut it. GHL dumps dozens of default contact keys in its payload, which can break APIs expecting a strict, minimal schema.
Drop a Custom Code action into your workflow instead. Here is a battle-tested snippet using native fetch to send a clean payload with auth headers:
const contactId = inputData.contactId;
const email = inputData.email;
const locationId = inputData.locationId;
const apiKey = inputData.apiKey; const payload = { lead_id: contactId, lead_email: email, source: 'ghl_workflow', timestamp: new Date().toISOString()
}; const response = await fetch('https://api.yourdomain.com/v1/leads', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'X-Location-ID': locationId, 'X-Source-System': 'GoHighLevel-Production' }, body: JSON.stringify(payload)
}); if (!response.ok) { const errorText = await response.text(); throw new Error(`API error (${response.status}): ${errorText}`);
} const result = await response.json();
output = { status: 'success', externalId: result.id };Before running this, declare your variables in the left sidebar of the Custom Code step. Map contactId to {{contact.id}}, email to {{contact.email}}, and locationId to your sub-account ID. If you need help grabbing that ID, check our guide on how to find sub-account location IDs in GoHighLevel.
Testing Webhook Headers with Webhook.site and cURL
Do not test outbound webhooks against your live production endpoint right away. Fire them at a temporary request inspector like Webhook.site first so you can verify the exact raw headers GHL sends.
Grab a unique URL from Webhook.site, paste it into your GHL webhook action, and trigger the workflow with a test contact. When the payload hits, check these three details:
- Authorization Header: Make sure there is exactly one space between
Bearerand your token. A missing or double space is an easy mistake that causes instant 401s. - Content-Type: Verify it shows
application/json. - Custom X-Headers: Check the header casing if your receiver is strict (most HTTP servers are case-insensitive, but older backends can be finicky).
To isolate issues between GHL and your target server, replicate the exact request from your terminal with cURL:
curl -X POST https://api.yourdomain.com/v1/leads -H "Content-Type: application/json" -H "Authorization: Bearer test_sec_token_9921" -H "X-Location-ID: loc_xyz123abc" -d '{"lead_email":"john@example.com","source":"ghl_workflow"}'If cURL succeeds instantly from your terminal but GHL’s webhook times out or fails, your endpoint’s firewall or Cloudflare rules might be blocking HighLevel’s outbound IP ranges.
Handling Authentication Tokens with API v2
If your webhook calls HighLevel’s own REST API v2 from inside a workflow (for example, to update another location or perform an agency-level query), you need two specific headers: Authorization: Bearer and Version: 2021-07-28.
Check the HighLevel API documentation for the exact endpoints and scopes required. We also have a breakdown on how to authenticate and pass location ID in GoHighLevel API v2 if you run into token scope errors.
Common Webhook Delivery Gotchas and Fixes
I’ve run into a handful of frustrating edge cases when firing webhooks with custom headers across client accounts. Here is what usually goes wrong:
- 400 Bad Request on nested payloads: The native webhook action sends a massive, pre-formatted contact JSON payload. If the receiving endpoint requires a flat or specific JSON structure, switch to Custom Code and send explicit keys. See our guide on sending custom values and contact fields in GoHighLevel webhooks.
- Special characters in headers: If your API key has symbols like
$,&, or quotes, merge tags can occasionally mangle them if pasted unquoted. Store them inside a clean Custom Value. - Execution timeouts: GHL workflow steps timeout if your target server takes longer than ~10 seconds to respond. If your API runs heavy processing, have it return an immediate
200 OKor202 Accepted, then process the payload asynchronously in a background worker. - Silent webhook drops: If the execution logs show the webhook action completed but nothing reached your server, review your enrollment triggers and filters using our guide on fixing GoHighLevel workflow webhooks not firing.
Receiving and Processing Responses
One major advantage of the Custom Code action over the standard Webhook action is capturing the response body and passing data downstream.
If your external API returns a generated ID or confirmation token, return it in the output object from your script:
// Inside Custom Code action
const apiResponse = await response.json(); output = { externalMemberId: apiResponse.member_id, portalUrl: apiResponse.portal_link, accountStatus: apiResponse.status
};In the next workflow step (like Update Contact Field), you can pull externalMemberId straight from the Custom Code action output in the merge tag dropdown. That gives you a clean, bidirectional sync without setting up intermediate middleware.
Frequently Asked Questions
Can I send dynamic authorization tokens from a contact field?
Yes. In the native Webhook action, enter Bearer {{contact.custom_field_key}} into the Header value column. In Custom Code, pass that field into the action’s input variables and reference it with a template literal.
What HTTP methods does HighLevel support for outbound webhooks?
The standard Webhook action handles POST and GET. If you need PUT, PATCH, or DELETE with custom headers, use the Custom Code action with Node’s fetch.
Does HighLevel retry failed outbound webhooks automatically?
The standard webhook action has built-in retry attempts for network hiccups, but you cannot customize backoff intervals or retry triggers. If guaranteed delivery is mandatory, wrap your requests in a Custom Code try/catch loop or send them to a queuing service like AWS SQS or Cloudflare Queues.
How do I send HMAC signature headers for payload verification?
Use the Custom Code action. The Node.js environment includes the native crypto module, so you can generate a SHA-256 HMAC hash of the payload using your secret key and attach it to the request headers before dispatching.
Next Steps
Once your outbound webhooks are delivering authenticated requests reliably, you might need to handle the return path. If your external systems need to push updates back into GHL, check out our walkthrough on how to trigger a GoHighLevel workflow with inbound webhooks.

