Your Claude Code dashboard owns every form, opt-in, landing page, and the sales-call screens. GoHighLevel owns only the workflows and the CRM data. This guide shows the GHL workflows to build node-for-node, and the exact integration contract that wires your dashboard to them.
One rule decides where everything lives: if a human looks at it, it's in your dashboard; if it's automation or CRM data, it's in GHL. The two never overlap. Your dashboard writes data into GHL and pulls data out of GHL through the API, and starts workflows by firing webhooks. GHL runs the automation and (optionally) webhooks results back.
This is the big component. Because all forms are third-party, your dashboard is now responsible for every write that a native GHL form used to do for free. Build to this contract and the workflows behave exactly as drawn. All calls are GHL API 2.0 (V1 is end-of-support).
| Use case | What your dashboard does | GHL API / mechanic | When |
|---|---|---|---|
| ① Opt-in / capture (public landing pages) | |||
| Create the lead | Upsert contact: first, last, email, phone, services, city, source, consent | POST /contacts/upsert (dedupe by email/phone) | On opt-in submit |
| Record source | Write Source / Lead Source field | Same upsert payload (custom field) | On opt-in submit |
| Capture consent | Store TCPA/A2P opt-in + timestamp on a field | Custom field on the contact | On opt-in submit |
| Start the dispatcher | Kick off Assignment Of Lead | Inbound Webhook trigger, or POST add-to-workflow | Last, after the writes |
| ② Lead claim (rep dashboard) | |||
| Find the right contact | Typeahead search so the claim ties to the real contact | GET /contacts/search | As rep types |
| Assign the user ★ | Set the contact's assigned user to the claiming rep | PUT /contacts/{id} assignedTo: userId | On claim |
| Assign the lead ★ | Also set the Assigned Sales Rep custom field + add the rep-name tag | Same PUT (custom field) + POST tag | On claim |
| Map rep → GHL user | Translate "John" to the GHL user ID | GET /users/?locationId (cache it) | Dashboard load |
| Start New Lead | Begin nurture/dial (this also cancels the round-robin) | Inbound Webhook / add-to-workflow | On claim |
| ③ Sales-call forms (rep dashboard) | |||
| Load the client | Search + pull the contact into the call screen | GET /contacts/search + GET /contacts/{id} | Call start |
| Save call data | Write City/Location + the 3 levers (Send Contract & Invoice, Sales Call Email, Disqualified) | PUT /contacts/{id} (custom fields) | On submit |
| Save notes | Sales-call notes onto the contact | POST /contacts/{id}/notes | On submit |
| Move the pipeline | Create/update opportunity, stage, value, packed name | POST /opportunities | On submit |
| Book follow-up | Put the follow-up date on a GHL calendar | POST /calendars/events/appointments | On submit |
| Start Sales Call workflow | Fire Sales Call [Part I] — submit LAST | Inbound Webhook | After all writes |
| ④ Platform / cross-cutting (always) | |||
| Render dropdowns | Reps, sources, pipeline stages, calendars, services | GET users / pipelines / calendars / custom fields | Dashboard load |
| Field-ID parity | Write to the exact field IDs the workflows read | From the discovery-script output | Build time |
| Auth | Hold a Private Integration token server-side (per location) | Bearer token, never in the browser | Always |
| Reliability | Write data first, fire workflow last; retry/queue webhooks | Idempotency keys; log failures | Every submit |
| Sync back (optional) | Keep the dashboard live when GHL changes something | GHL outbound Webhook action → your app | On GHL events |
The workflows below live inside GHL. Build each node top to bottom. The blue node is the Inbound Webhook your dashboard fires; everything after it is standard GHL actions. The callout beside each card explains what ▸, why ★, customize ✦, and where the API touches 🔌.
Goal: clean the new contact, broadcast a claimable lead, and guarantee assignment within minutes (a rep claims it in your dashboard, or GHL round-robins it). Your dashboard starts this by firing its Inbound Webhook after the opt-in upsert.
Your landing-page form upserts the contact via the API, then calls this webhook URL with the contact ID. That starts the workflow. This replaces the old "Form Submitted: New Lead Form" trigger.
Add trigger → Inbound Webhook → copy the URL → paste into your dashboard's "create lead" handler as the final step.
Clears any old Do-Not-Disturb so a returning lead can be texted/emailed again.
Wipes contract lost — they're a fresh lead again.
Stamps new lead for stage + segmentation.
Condition: Tags Includes contract won.
Don't drip a "new lead" sequence at an existing client.
Email the team, drop them out. Call them instead.
Use Send Internal Notification → Email (not Slack).
Each branch updates Contact Source + alerts the team.
Sources differ per business. Default: Facebook Ads + SEO; add Google Ads, referral, etc.
Tells the whole team a lead is up for grabs.
The claim is no longer a GHL link — the rep claims in your dashboard, which assigns + starts New Lead via the API (see Section 2 + the contract).
Tracks the lead. Pack the name: {{contact.name}} | {{contact.city}} | {{contact.services}}.
Top-rep head start before the team gets re-pinged.
Grace window. Still unclaimed → auto-assign next.
Nobody claimed → round-robin so the lead never dies.
This fallback stays a GHL action. Your dashboard doesn't touch it — but a dashboard claim that starts New Lead removes the contact here before the 3-min timer, so it's skipped.
automatically assigned = the switch New Lead reads. Auto = SMS only. A dashboard claim writes the rep-name tag instead, not this one.
Hands off to New Lead (Section 2) for the call + nurture.
| What happened | How it's set | Tag written | Effect in New Lead |
|---|---|---|---|
| A rep claims in your dashboard | Dashboard sets assignedTo + Assigned Sales Rep field, then starts New Lead (which removes from this workflow) | [rep name] | Manual → auto-dials the rep |
| Nobody claims (3-min elapsed) | GHL round-robin Assign User fires | automatically assigned | Auto → SMS only, no call |
No GHL trigger — your dashboard starts it on claim (via webhook), or Assignment Of Lead adds it on auto-assign. Goal: lock in the right rep, dial when a human's available, nurture until they reply.
Cancels the round-robin timer so a claimed lead isn't reassigned.
Someone already claimed it — round-robining again would steal it.
Workflow contact Is NOT active in Sales Call [Part I] AND Sales Call [Part II].
Don't text "book a call" right after they had one. None → END.
Per rep — Assigned user Includes [rep] AND Assigned Sales Rep is not empty → keep that rep.
A past client goes back to the rep who closed them. This block only checks if a name is already attached.
e.g. Assigned Sales Rep is “John” AND Assigned user is empty → Assign to user: John.
Your claim already set assignedTo + the Assigned Sales Rep field. This block is the safety net that pins it inside GHL if the native owner was left empty. Add one branch per rep (John, Alex, Joe …).
Condition: Tags Includes automatically assigned. Fire an Internal Email: “[rep] assigned to [name].”
A beat so it's not robotic, then add the source tag.
Nobody's available (2 a.m.) → point to the calendar, not "chat now."
Calls the rep, plays a whisper: “Hey {{user.first_name}}, {{contact.name}} inquired about {{contact.services}} — press any key to connect to {{contact.first_name}}.” Keypress → bridges to the lead. Timeout 60s.
Live lead on the line with zero dialing.
Answer → Stop on Response removes them. No answer → this SMS. Manual copy says "chat now" (a rep is available).
Wait 1 min → Email #1. Wait 1 day → Email #2. Another day → next, widening the gap.
Every email's one job: book a call. Add lead magnets; split-test once live.
This is the part most third-party builds get wrong. The New Lead workflow reads two different "owner" signals, so a claim has to write both.
PUT /contacts/{id} with assignedTo: <repUserId>. Read by the "Assigned user" conditions.POST the rep's tag (so it's NOT the auto-assigned path).Block #1 (keep-existing) checks Assigned user + Assigned Sales Rep is not empty — that's the returning-client case. Block #2 (assign-claimer) checks Assigned Sales Rep is [rep] + Assigned user is empty. If your claim only set the native owner, block #2 never fires and the lead can land unassigned in the field the rest of the system trusts. Write both, every time.
| Problem | Fix |
|---|---|
| Claim creates a duplicate contact | Use upsert / search-by-email before write. Never blind-create. |
| Lead lands unassigned in reports | You set assignedTo but not the Assigned Sales Rep field. Write both. |
| Workflow didn't start | You fired the webhook before the contact existed. Upsert first, webhook last. |
| Auto-assigned leads still get called | The fork reads the wrong tag — confirm automatically assigned. |
| Round-robin skips reps | Turn OFF "Only apply to unassigned" on the Assign User action. |
GHL already handles A2P texting, call bridging, deliverability, and scheduling. Rebuilding those is wasted effort. Build the front end; let GHL be the engine.
Either works. Inbound Webhook is simplest to wire and is what the diagrams show. Add-to-Workflow is handy when you already have the contact ID and want an explicit call.
Users (reps), custom fields, pipelines/stages, calendars, tags, and contacts — to render dropdowns and write to the right IDs. See the contract, group ④.
Yes — same pattern: search the contact, write fields/notes/opportunity/appointment, then fire the Sales Call workflow last. See the contract, group ③.
Optional enrichment for multi-region businesses. Removed to keep the teaching build fast; add back if you operate across regions.
Your dashboard talks to GHL using the hidden internal IDs behind your assets, not their names (a field called "Assigned Sales Rep" is really aZ8k2Lp9… under the hood). You have two ways to get them: paste the prompt below into GHL Ask AI (fastest, gets everything at once), or grab each one by hand from GHL using the click-paths below.
| Asset | Need it? | Why |
|---|---|---|
| Location ID | ✅ Yes ×1 | Every API call is scoped to the sub-account |
| Workflow IDs | ✅ Yes ×5 | Start workflows from the dashboard + the "not active in" checks |
| Custom Field IDs (+ key + options) | ✅ Yes ×7 | The dashboard writes to these exact fields |
| Pipeline ID + Stage IDs | ✅ Yes ×1 | Create/move the opportunity into the right stage |
| Calendar ID | ✅ Yes ×1 | Book the follow-up appointment via API |
| User IDs | ✅ Yes ×rep | assignedTo needs the user ID, not the name |
| Tag IDs | ❌ No | The API adds/removes tags by name — just keep the exact spelling |
| Trigger links | ❌ No | The claim now happens in your dashboard; the GHL claim link is retired |
| Private Integration token | 🔑 Auth | Not an ID — the runtime key your dashboard uses. Create once, store server-side |
Most IDs are the long string in your browser's address bar when you have that item open. The two GHL hides (field + stage IDs) are why Ask AI is the easy button.
| Asset | Click-path in GHL | Shows the ID? |
|---|---|---|
| Location ID | Settings → Business Profile, or read the address bar after /location/ | ✅ URL |
| Workflow ID | Automation → Workflows → open the workflow → it's the long string in the address bar | ✅ URL |
| Pipeline ID | Opportunities → open the pipeline → address bar | ◑ Pipeline in URL; stages hidden → Ask AI |
| Calendar ID | Calendars → Calendar Settings → open the calendar → address bar / booking link | ✅ URL |
| User ID | Settings → My Staff (Team) → open the person → address bar | ✅ URL |
| Custom Field ID | Settings → Custom Fields → open the field. You'll see the field key (e.g. contact.type_of_source) but usually not the raw ID | ❌ Hidden → Ask AI |
Replace every [bracketed] name with your real asset name, then paste the whole thing into GHL → Ask AI. It pulls only the assets you name, so it never truncates the way a full-account dump does. Anything it can't find by that exact name comes back as NOT FOUND so you know to fix the spelling.
Paste the results back to whoever's wiring the dashboard. With these IDs every row of the integration contract becomes a literal API call (upsert contact, set assignedTo + Assigned Sales Rep, create the opportunity at the New Lead stage, book the follow-up, start the workflow). Tag names you already have.
| Version | Date | Changes |
|---|---|---|
| 2.1 | Jun 29, 2026 | Added the Appendix — Pull your GHL IDs: what to look up vs. skip, the manual click-paths in GHL for each ID, and the copy-paste Ask AI lookup prompt (templated with fill-in asset names). |
| 2.0 | Jun 29, 2026 | Rebuilt for third-party architecture: all forms/pages in the Claude Code dashboard, GHL = workflow engine. Added the architecture split + full integration contract + assign-user deep dive. Trigger → Inbound Webhook; claim → API. Renamed rep example Jono → John. |
| 1.0 | Jun 29, 2026 | Initial native-GHL teaching build (simplified from the full snapshot). |