Custom Values and Custom Fields in GoHighLevel Webhooks: Full Setup Guide

by Fahim

When you trigger an outbound webhook in GoHighLevel, the default contact dump is a mess. It spits out every standard property GHL tracks, while your custom fields get buried in nested arrays or dropped entirely. If your receiving API expects clean, predictable JSON keys, parsing the default payload is a headache.

You don’t have to settle for the default data dump. By using the Custom Data mapping table in GoHighLevel workflows, you can build clean JSON payloads, mix contact-level custom fields with account-level custom values, and sanitize empty values before they break your downstream endpoints.

Custom Values and Custom Fields in GoHighLevel Webhooks: Full Setup Guide
Custom Values and Custom Fields in GoHighLevel Webhooks: Full Setup Guide

The Difference Between Custom Values and Custom Fields in Webhooks

They look nearly identical in the workflow merge tag picker, but they pull from completely different scopes in the HighLevel database. Mix them up, and your webhook will fire with empty strings or literal unparsed tags like {{ contact.custom_field }}.

  • Custom Fields (Contact Scope): Stored on the individual contact record. These hold dynamic lead data like deal_size, preferred_onboarding_date, or an industry tag. Every contact has their own distinct value (or null).
  • Custom Values (Location Scope): Global account-level variables configured under Settings > Custom Values. These are static across the entire sub-account—things like your internal API auth token, support email, company timezone, or a base webhook URL.

When you need to send contact data alongside an agency identifier or a shared API token, you combine both scopes inside the same outbound payload.

Setting Up Custom Data in Your HighLevel Account

Before touching the workflow builder, double-check the actual system keys assigned to your fields. HighLevel generates backend keys that don’t always match the user-facing field label you created.

Go to Settings > Custom Fields, find your target field, and click the edit icon to confirm the exact key. Then head over to Settings > Custom Values to check your account-level variables. For this setup, we’ll assume you have custom values named Webhook_Secret and Agency_Slug.

If you’re having trouble locating your sub-account ID while writing custom scripts, check out how to find your sub-account location ID in GoHighLevel.

Configuring the Custom Data Webhook Action in Workflows

The plain Webhook action in GHL sends a bloated, unformatted POST request containing the entire contact object. To send lean, explicit keys, use the Custom Data table inside the webhook step.

Here is the setup in your workflow editor:

  1. Add your trigger (e.g., Contact Tag Added, Form Submitted, or Opportunity Status Changed).
  2. Add a new action and select Webhook.
  3. Set the method to POST and paste your endpoint URL.
  4. Scroll down to the Custom Data table.

Instead of passing raw database dumps, map your desired JSON keys on the left and the HighLevel merge tags on the right:

{ "contact_id": "{{contact.id}}", "email": "{{contact.email}}", "first_name": "{{contact.first_name}}", "lead_score": "{{contact.lead_score}}", "company_tier": "{{contact.account_tier}}", "agency_identifier": "{{custom_values.agency_slug}}", "auth_token": "{{custom_values.webhook_secret}}"
}

When the workflow fires, GHL resolves those merge tags against the database record, builds a flat JSON body from your key-value pairs, and posts it to your server.

Testing Payload Resolution with an Endpoint Inspector

Don’t send test webhooks straight to your production backend right away. If a contact record is missing a field, merge tags can fail silently or send unexpected types.

I always throw a temporary URL from Webhook.site into the workflow action first. Set the workflow to Publish, enroll a test contact, and inspect the raw incoming headers and body.

Here is what a properly resolved Custom Data payload looks like when it hits your inspector:

{ "contact_id": "zR8bNm9K10xV2pLqA3s5", "email": "alex.rivers@example.com", "first_name": "Alex", "lead_score": "85", "company_tier": "Enterprise", "agency_identifier": "growth-marketing-sub1", "auth_token": "sec_live_998124018274"
}

If the request never arrives at your test URL, double-check your trigger filters against our guide on fixing GoHighLevel webhooks not firing.

Handling Missing Custom Fields with Merge Field Fallbacks

A frequent problem with GHL webhooks is missing data. If a lead skipped an optional form field like lead_score, HighLevel might output an empty string "" or leave the tag unparsed depending on how the workflow was triggered.

If your receiving API strictly requires an integer or boolean, an empty string will throw a 400 Bad Request or crash your parser. You can prevent this by defining inline fallback values in your merge syntax.

Take a look at our guide on GoHighLevel merge fields and fallback values to format strings like {{ contact.account_tier | default: 'Standard' }} cleanly.

Parsing Webhook Payloads in Node.js and Express

When building a Node.js receiver for HighLevel webhooks, always validate your auth token first, then pull out and sanitize your custom fields before doing any database writes.

Here is an Express route handler that validates the custom value secret and extracts the contact payload:

const express = require('express');
const app = express(); app.use(express.json()); const EXPECTED_SECRET = process.env.GHL_WEBHOOK_SECRET || 'sec_live_998124018274'; app.post('/api/ghl-webhook', (req, res) => { const payload = req.body; // Validate the custom value auth token passed from GHL if (payload.auth_token !== EXPECTED_SECRET) { return res.status(401).json({ error: 'Unauthorized webhook request' }); } const contactId = payload.contact_id; const leadScore = parseInt(payload.lead_score, 10) || 0; const companyTier = payload.company_tier || 'Standard'; console.log(`Processing contact ${contactId}: Tier=${companyTier}, Score=${leadScore}`); // Perform your downstream processing here return res.status(200).json({ status: 'success', received_id: contactId });
}); app.listen(3000, () => { console.log('GoHighLevel webhook listener running on port 3000');
});

If you’re processing high webhook volume from bulk campaign updates, don’t handle database writes inline. Read how to prevent duplicate webhook processing with Redis and Express to keep your receiver fast and idempotent.

Gotchas When Sending Custom Field Data Over Webhooks

A few quirks I’ve run into across production GHL setups:

  • Multi-Select and Checkbox Fields: HighLevel sends multi-select fields as comma-separated strings (like "Option A, Option B") when mapped through Custom Data. If your database expects an array of strings, you’ll need to split on the comma in your receiving logic.
  • Date Formatting: Custom date fields format according to the sub-account’s local timezone settings (often MM/DD/YYYY). If your backend expects ISO-8601 timestamps, parse and normalize them on ingest.
  • Snapshot Field Key Collisions: When importing snapshot templates into new sub-accounts, HighLevel sometimes renames colliding custom fields behind the scenes (e.g., appending _1 or _2). Always verify merge tags in the dropdown picker after deploying a snapshot.

For more details on payload specifications and API scopes, check out the official HighLevel API documentation and the HighLevel Webhook Workflow Help Center.

Frequently Asked Questions

Can I send nested JSON objects inside GoHighLevel Custom Data?

The standard Custom Data table only outputs flat, top-level key-value pairs. If your endpoint expects deeply nested JSON (like billing.address.city), either pass dot-notated keys and unflatten them on your server, or use an intermediate webhook proxy to reshape the payload.

Why is my custom value sending the raw merge tag text instead of the value?

This happens when there is a typo in the tag name or if the custom value was deleted from Settings > Custom Values. HighLevel won’t throw a workflow error; it just outputs the unparsed tag string (like {{custom_values.my_var}}). Pick the tag directly from the merge field dropdown to avoid typos.

What HTTP method does the GoHighLevel Webhook action use?

The standard Webhook workflow action always sends an HTTP POST request with Content-Type: application/json. If you need GET, PUT, or custom headers, use the Custom Webhook / HTTP Request action instead.

How do I secure my GoHighLevel webhook endpoints?

HighLevel doesn’t offer native HMAC SHA-256 webhook signatures on standard workflow actions. The most reliable workaround is storing a secret token in Custom Values, mapping it as a key in your Custom Data table, and verifying that token on your receiving server before processing the request.

all_in_one_marketing_tool