@elitedcs/ghl-mcp 3.65.2 → 3.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/dist/index.js +312 -65
- package/guide/guide.html +118 -5
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.66.0 — webhooks work now
|
|
4
|
+
|
|
5
|
+
The five webhook tools have never worked for anyone. They called an address
|
|
6
|
+
GoHighLevel does not have, so every call returned "not found" no matter what you
|
|
7
|
+
asked for. They are rebuilt, and this time each step was proven against a real
|
|
8
|
+
account before it was written down.
|
|
9
|
+
|
|
10
|
+
GoHighLevel does not offer a way to manage webhooks directly. What it does offer
|
|
11
|
+
is a workflow step that sends data out, so that is what these tools now build.
|
|
12
|
+
A webhook is a small workflow in the client's own account, which turns out to be
|
|
13
|
+
better than the thing that was missing:
|
|
14
|
+
|
|
15
|
+
- **It can fire on almost anything**, not just a new contact. A tag added, a
|
|
16
|
+
pipeline stage changed, an appointment no showed, a call missed.
|
|
17
|
+
- **The client can see it.** It sits in their Automation list where they can read
|
|
18
|
+
it, pause it, and edit it, instead of being invisible plumbing.
|
|
19
|
+
- **You can add your own fields and your own headers**, and merge fields fill in,
|
|
20
|
+
so the contact's first name arrives already filled. All of that works on every
|
|
21
|
+
plan.
|
|
22
|
+
|
|
23
|
+
**Two things worth knowing before you point one at production**, both measured
|
|
24
|
+
live rather than assumed:
|
|
25
|
+
|
|
26
|
+
- **`tags` arrives as one line of comma separated text, not a list.** Code that
|
|
27
|
+
checks whether it contains "vip" will also match a contact tagged
|
|
28
|
+
"vip-waitlist". This is in every one of the tool descriptions now.
|
|
29
|
+
- **If the receiving app is down, GoHighLevel keeps retrying**, about five
|
|
30
|
+
minutes later and then about ten. Nothing is thrown away. But the contact waits
|
|
31
|
+
at that step, and so does every step after it. If the send is not critical, put
|
|
32
|
+
it at the end of a workflow or give it a workflow of its own.
|
|
33
|
+
|
|
34
|
+
Deleting a webhook refuses to run if that workflow also does other things, so
|
|
35
|
+
"delete the webhook" can never quietly take a client's nurture sequence with it.
|
|
36
|
+
|
|
37
|
+
New guide: **Send GHL data to another app.** Guide coverage is now 39 of 50.
|
|
38
|
+
|
|
39
|
+
## 3.65.3 — the guide now covers most of what you are paying for
|
|
40
|
+
|
|
41
|
+
The library had guides for 20 of the 50 areas the product touches. Six shipped
|
|
42
|
+
guides were listed as covering things they never actually mentioned. That is
|
|
43
|
+
now 38, and the new material was checked against two real accounts before it
|
|
44
|
+
was written, not recalled.
|
|
45
|
+
|
|
46
|
+
- **Your team, numbers, and files** are covered in account admin: who has access,
|
|
47
|
+
which phone numbers the account owns, what is in media storage.
|
|
48
|
+
- **Surveys** are covered alongside forms, including why "show me my forms" never
|
|
49
|
+
returns them and where a client's missing intake answers usually are.
|
|
50
|
+
- **Email campaigns** are covered alongside templates, including how to spot an
|
|
51
|
+
old sequence still sending against a client's list.
|
|
52
|
+
- **Review links, connected social accounts and bulk campaigns** each got the
|
|
53
|
+
section their guide had been promising.
|
|
54
|
+
|
|
55
|
+
**Webhooks do not work and never did.** All five webhook tools call an address
|
|
56
|
+
GoHighLevel does not have. Four different paths, two API versions, two accounts:
|
|
57
|
+
404 every time. They are now marked as not guide material rather than left
|
|
58
|
+
looking available. Whether they get repaired or withdrawn is a separate call.
|
|
59
|
+
|
|
60
|
+
The billing guide added in 3.65.2 also covers estimates now, so the second
|
|
61
|
+
half-written billing guide has been retired rather than left on the shelf.
|
|
62
|
+
|
|
3
63
|
## 3.65.2 — invoicing never worked, and the guide stopped being one long page
|
|
4
64
|
|
|
5
65
|
- **Fix: creating an invoice failed every single time.** The call sent five
|
package/dist/index.js
CHANGED
|
@@ -2854,7 +2854,7 @@ var require_package = __commonJS({
|
|
|
2854
2854
|
"package.json"(exports2, module2) {
|
|
2855
2855
|
module2.exports = {
|
|
2856
2856
|
name: "@elitedcs/ghl-mcp",
|
|
2857
|
-
version: "3.
|
|
2857
|
+
version: "3.66.0",
|
|
2858
2858
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
2859
2859
|
description: "GoHighLevel MCP Server for Claude. 238 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
2860
2860
|
main: "dist/index.js",
|
|
@@ -5757,6 +5757,41 @@ function validateActionChain(actions, existingIds) {
|
|
|
5757
5757
|
case "remove_from_workflow":
|
|
5758
5758
|
if (!attr.workflowId || !Array.isArray(attr.workflow_id)) throw new Error(`Remove from workflow action "${action.name}" needs BOTH 'workflowId' string AND 'workflow_id' array.`);
|
|
5759
5759
|
break;
|
|
5760
|
+
case "webhook": {
|
|
5761
|
+
if (typeof attr.url !== "string" || !attr.url.trim()) {
|
|
5762
|
+
throw new Error(`Webhook action "${action.name}" needs a 'url' in attributes.`);
|
|
5763
|
+
}
|
|
5764
|
+
if (!/^https?:\/\//i.test(attr.url.trim())) {
|
|
5765
|
+
throw new Error(
|
|
5766
|
+
`Webhook action "${action.name}" has a url that does not start with http:// or https:// ("${attr.url}"). GHL saves it and then never fires it.`
|
|
5767
|
+
);
|
|
5768
|
+
}
|
|
5769
|
+
if (typeof attr.method !== "string" || !attr.method.trim()) {
|
|
5770
|
+
throw new Error(`Webhook action "${action.name}" needs a 'method' in attributes (POST, GET or PUT).`);
|
|
5771
|
+
}
|
|
5772
|
+
if (!["POST", "GET", "PUT"].includes(attr.method.trim().toUpperCase())) {
|
|
5773
|
+
throw new Error(
|
|
5774
|
+
`Webhook action "${action.name}" has method "${attr.method}". GHL's builder only offers POST, GET and PUT.`
|
|
5775
|
+
);
|
|
5776
|
+
}
|
|
5777
|
+
for (const field of ["customData", "headers"]) {
|
|
5778
|
+
const entries = attr[field];
|
|
5779
|
+
if (entries === void 0) continue;
|
|
5780
|
+
if (!Array.isArray(entries)) {
|
|
5781
|
+
throw new Error(`Webhook action "${action.name}" needs '${field}' to be an array of {key, value} pairs.`);
|
|
5782
|
+
}
|
|
5783
|
+
for (const entry of entries) {
|
|
5784
|
+
const key = isRecord(entry) ? entry.key : void 0;
|
|
5785
|
+
const value = isRecord(entry) ? entry.value : void 0;
|
|
5786
|
+
if (typeof key !== "string" || !key.trim() || value === void 0 || value === null) {
|
|
5787
|
+
throw new Error(
|
|
5788
|
+
`Webhook action "${action.name}" has an empty '${field}' pair. Every entry needs a non-empty key and a value.`
|
|
5789
|
+
);
|
|
5790
|
+
}
|
|
5791
|
+
}
|
|
5792
|
+
}
|
|
5793
|
+
break;
|
|
5794
|
+
}
|
|
5760
5795
|
}
|
|
5761
5796
|
}
|
|
5762
5797
|
for (const action of actions) {
|
|
@@ -10041,93 +10076,300 @@ function registerCouponTools(server2, client) {
|
|
|
10041
10076
|
|
|
10042
10077
|
// src/tools/webhooks.ts
|
|
10043
10078
|
var import_zod34 = require("zod");
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10079
|
+
var WEBHOOK_PAYLOAD_NOTE = "PAYLOAD GHL SENDS (captured live, not from docs): contact_id, first_name, last_name, full_name, email, phone, tags, country, date_created, contact_source, full_address, contact_type, plus a `location` object, a `workflow` object ({id, name}), `triggerData`, and your `customData`. Sent as application/json. WATCH OUT: `tags` arrives as a COMMA-SEPARATED STRING (\"vip,newsletter\"), NOT an array \u2014 splitting is on you, and a substring check like tags.includes('vip') matches 'vip-waitlist' too. `triggerData` was empty for a contact_tag trigger.";
|
|
10080
|
+
var WEBHOOK_FAILURE_NOTE = "IF YOUR ENDPOINT IS DOWN (measured live against an HTTP 500, with a control run on the same workflow; GHL documents none of this): GHL does NOT silently drop the call. It retries, with a doubling gap of roughly 5 minutes then roughly 10. How many times it retries before giving up is UNKNOWN \u2014 the test ended first. The finding that matters more: the contact STOPS at that step and never reaches the rest of the workflow. In the controlled pair, the contact whose webhook returned 200 completed every later step; the contact whose webhook returned 500 never reached the next step at all. So a dead receiver does not lose data, it halts the workflow for every contact flowing through it. If the webhook is not critical, put it at the END of a workflow, or give it a workflow of its own, so an outage cannot hold up the steps that matter.";
|
|
10081
|
+
var ALLOWED_METHODS = ["POST", "GET", "PUT"];
|
|
10082
|
+
var KeyValueSchema = import_zod34.z.object({
|
|
10083
|
+
key: import_zod34.z.string().describe("Key name. Cannot be empty."),
|
|
10084
|
+
value: import_zod34.z.string().describe("Value. Merge fields such as {{contact.first_name}} resolve at send time.")
|
|
10085
|
+
});
|
|
10086
|
+
var EventSchema = import_zod34.z.union([
|
|
10087
|
+
import_zod34.z.string().describe("A GHL trigger type, e.g. 'contact_created'. Fires with no filter."),
|
|
10088
|
+
import_zod34.z.object({
|
|
10089
|
+
type: import_zod34.z.string().describe("A GHL trigger type, e.g. 'contact_tag' or 'pipeline_stage_updated'."),
|
|
10090
|
+
conditions: import_zod34.z.array(import_zod34.z.record(import_zod34.z.unknown())).optional().describe(
|
|
10091
|
+
"Filter conditions, exactly as update_workflow_actions takes them. Use get_trigger_registry to find the valid fields for a trigger type. Omit to fire on every event of that type."
|
|
10092
|
+
)
|
|
10093
|
+
})
|
|
10094
|
+
]);
|
|
10095
|
+
function buildWebhookAction(input) {
|
|
10096
|
+
const url = (input.url ?? "").trim();
|
|
10097
|
+
if (!url) throw new Error("A webhook needs a 'url'.");
|
|
10098
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
10099
|
+
throw new Error(
|
|
10100
|
+
`Webhook url must start with http:// or https:// (got "${url}"). GHL saves any string here and then never fires.`
|
|
10101
|
+
);
|
|
10102
|
+
}
|
|
10103
|
+
const method = (input.method ?? "POST").toUpperCase();
|
|
10104
|
+
if (!ALLOWED_METHODS.some((m) => m === method)) {
|
|
10105
|
+
throw new Error(`Webhook method must be one of POST, GET, PUT (got "${input.method}").`);
|
|
10106
|
+
}
|
|
10107
|
+
const pairs = (entries, field) => {
|
|
10108
|
+
const list = entries ?? [];
|
|
10109
|
+
for (const entry of list) {
|
|
10110
|
+
if (!entry || typeof entry.key !== "string" || !entry.key.trim()) {
|
|
10111
|
+
throw new Error(`Every ${field} entry needs a non-empty key. GHL only warns about this, then delivers nothing.`);
|
|
10112
|
+
}
|
|
10113
|
+
if (entry.value === void 0 || entry.value === null) {
|
|
10114
|
+
throw new Error(`${field} entry "${entry.key}" needs a value.`);
|
|
10115
|
+
}
|
|
10116
|
+
}
|
|
10117
|
+
return list;
|
|
10118
|
+
};
|
|
10119
|
+
const action = {
|
|
10120
|
+
name: input.name?.trim() || "Webhook",
|
|
10121
|
+
type: "webhook",
|
|
10122
|
+
cat: "actions",
|
|
10123
|
+
attributes: {
|
|
10124
|
+
method,
|
|
10125
|
+
url,
|
|
10126
|
+
customData: pairs(input.customData, "customData"),
|
|
10127
|
+
headers: pairs(input.headers, "headers")
|
|
10128
|
+
}
|
|
10129
|
+
};
|
|
10130
|
+
return input.id ? { ...action, id: input.id } : action;
|
|
10131
|
+
}
|
|
10132
|
+
function buildWebhookTriggers(events) {
|
|
10133
|
+
if (!events?.length) {
|
|
10134
|
+
throw new Error("A webhook needs at least one event \u2014 a workflow with no trigger never fires.");
|
|
10135
|
+
}
|
|
10136
|
+
return events.map((event) => {
|
|
10137
|
+
const type = typeof event === "string" ? event : event.type;
|
|
10138
|
+
if (!type?.trim()) throw new Error("Each event needs a trigger 'type'.");
|
|
10139
|
+
const conditions = typeof event === "string" ? [] : event.conditions ?? [];
|
|
10140
|
+
return WorkflowTriggerSchema.parse({ name: type, type, conditions });
|
|
10141
|
+
});
|
|
10142
|
+
}
|
|
10143
|
+
var KeyValueListSchema = import_zod34.z.array(KeyValueSchema).catch([]);
|
|
10144
|
+
function readKeyValues(value) {
|
|
10145
|
+
const LooseEntry = import_zod34.z.object({ key: import_zod34.z.unknown(), value: import_zod34.z.unknown() }).transform((entry) => ({ key: String(entry.key ?? ""), value: String(entry.value ?? "") }));
|
|
10146
|
+
return KeyValueListSchema.parse(import_zod34.z.array(LooseEntry).catch([]).parse(value));
|
|
10147
|
+
}
|
|
10148
|
+
function replaceWebhookAttributes(templates, target, attributes) {
|
|
10149
|
+
return templates.map((action) => {
|
|
10150
|
+
if (!action.id || action.id !== target.id) return action;
|
|
10151
|
+
if (action.type !== "webhook") return action;
|
|
10152
|
+
return { ...action, attributes };
|
|
10153
|
+
});
|
|
10154
|
+
}
|
|
10155
|
+
function readId(workflow) {
|
|
10156
|
+
const record = workflow;
|
|
10157
|
+
const id = workflow._id ?? record.id;
|
|
10158
|
+
return typeof id === "string" ? id : "";
|
|
10159
|
+
}
|
|
10160
|
+
function summarizeWebhookWorkflow(workflow) {
|
|
10161
|
+
const templates = workflow.workflowData?.templates ?? [];
|
|
10162
|
+
const webhookActions = templates.filter((action) => action.type === "webhook");
|
|
10163
|
+
if (!webhookActions.length) return null;
|
|
10164
|
+
const actions = webhookActions.map((action) => {
|
|
10165
|
+
const attributes = action.attributes ?? {};
|
|
10166
|
+
return {
|
|
10167
|
+
actionId: action.id,
|
|
10168
|
+
name: action.name,
|
|
10169
|
+
url: typeof attributes.url === "string" ? attributes.url : "",
|
|
10170
|
+
method: typeof attributes.method === "string" ? attributes.method : "",
|
|
10171
|
+
customData: readKeyValues(attributes.customData),
|
|
10172
|
+
headers: readKeyValues(attributes.headers)
|
|
10173
|
+
};
|
|
10174
|
+
});
|
|
10175
|
+
return {
|
|
10176
|
+
webhookId: readId(workflow),
|
|
10177
|
+
name: workflow.name,
|
|
10178
|
+
status: workflow.status,
|
|
10179
|
+
events: (workflow.triggers ?? []).map((trigger) => trigger.type),
|
|
10180
|
+
actions,
|
|
10181
|
+
hasOtherActions: templates.length > webhookActions.length
|
|
10182
|
+
};
|
|
10183
|
+
}
|
|
10184
|
+
var WorkflowListSchema = import_zod34.z.object({ rows: import_zod34.z.array(import_zod34.z.object({ _id: import_zod34.z.string().optional(), id: import_zod34.z.string().optional() }).passthrough()).optional() }).passthrough();
|
|
10185
|
+
function registerWebhookTools(server2, builderClient) {
|
|
10186
|
+
const requireClient = () => {
|
|
10187
|
+
if (!builderClient) {
|
|
10188
|
+
throw new Error(
|
|
10189
|
+
"Webhooks run on GHL's workflow builder, which needs Firebase credentials. Run enable_workflow_builder (or health_check) to set them up."
|
|
10190
|
+
);
|
|
10191
|
+
}
|
|
10192
|
+
return builderClient;
|
|
10193
|
+
};
|
|
10194
|
+
const loadWebhooks = async (client, limit) => {
|
|
10195
|
+
const listed = WorkflowListSchema.parse(await client.listWorkflows(limit, 0));
|
|
10196
|
+
const rows = listed.rows ?? [];
|
|
10197
|
+
const webhooks = [];
|
|
10198
|
+
for (const row of rows) {
|
|
10199
|
+
const id = row._id ?? row.id;
|
|
10200
|
+
if (!id) continue;
|
|
10201
|
+
const summary = summarizeWebhookWorkflow(await client.getWorkflow(id));
|
|
10202
|
+
if (summary) webhooks.push(summary);
|
|
10203
|
+
}
|
|
10204
|
+
return { webhooks, scanned: rows.length };
|
|
10205
|
+
};
|
|
10206
|
+
server2.tool(
|
|
10047
10207
|
"list_webhooks",
|
|
10048
|
-
"List
|
|
10208
|
+
"List every webhook in the current sub-account. A webhook here is a workflow that fires a `webhook` action, so this also finds ones built by hand in the GHL UI. Returns the URL, HTTP method and triggering events for each. NOTE: GoHighLevel has no webhook REST API (it is Marketplace-OAuth only) \u2014 webhooks live on the workflow layer, which is why each one has a workflow ID and shows up in the client's Automation list. " + WEBHOOK_PAYLOAD_NOTE,
|
|
10049
10209
|
{
|
|
10050
|
-
|
|
10210
|
+
limit: import_zod34.z.number().int().positive().max(200).optional().describe(
|
|
10211
|
+
"How many workflows to scan. Defaults to 50, capped at 200. Each one is opened individually, so a large scan takes a while \u2014 the response says how many were covered and whether more remain."
|
|
10212
|
+
)
|
|
10051
10213
|
},
|
|
10052
|
-
async ({
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10214
|
+
async ({ limit }) => {
|
|
10215
|
+
try {
|
|
10216
|
+
const client = requireClient();
|
|
10217
|
+
const { webhooks, scanned } = await loadWebhooks(client, limit ?? 50);
|
|
10218
|
+
return jsonResponse({
|
|
10219
|
+
webhooks,
|
|
10220
|
+
_scan: {
|
|
10221
|
+
workflowsScanned: scanned,
|
|
10222
|
+
note: scanned >= (limit ?? 50) ? `Scanned the first ${scanned} workflows \u2014 there may be more. Raise 'limit' to cover them.` : `Scanned all ${scanned} workflows in this sub-account.`
|
|
10223
|
+
}
|
|
10224
|
+
});
|
|
10225
|
+
} catch (error) {
|
|
10226
|
+
return errorResponse(error);
|
|
10227
|
+
}
|
|
10057
10228
|
}
|
|
10058
10229
|
);
|
|
10059
|
-
|
|
10060
|
-
server2,
|
|
10230
|
+
server2.tool(
|
|
10061
10231
|
"get_webhook",
|
|
10062
|
-
"
|
|
10232
|
+
"Get one webhook by ID (the ID is its workflow ID, as returned by list_webhooks or create_webhook). Shows the destination URL, method, custom data, custom headers and triggering events. " + WEBHOOK_PAYLOAD_NOTE + " " + WEBHOOK_FAILURE_NOTE,
|
|
10063
10233
|
{
|
|
10064
|
-
webhookId: import_zod34.z.string().describe("The webhook ID
|
|
10065
|
-
locationId: import_zod34.z.string().optional().describe("GHL Location ID. Optional if GHL_LOCATION_ID is set in env.")
|
|
10234
|
+
webhookId: import_zod34.z.string().describe("The webhook ID \u2014 the workflow ID that carries the webhook action.")
|
|
10066
10235
|
},
|
|
10067
|
-
async ({ webhookId
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
|
|
10236
|
+
async ({ webhookId }) => {
|
|
10237
|
+
try {
|
|
10238
|
+
const client = requireClient();
|
|
10239
|
+
const summary = summarizeWebhookWorkflow(await client.getWorkflow(webhookId));
|
|
10240
|
+
if (!summary) {
|
|
10241
|
+
return jsonResponse({
|
|
10242
|
+
error: `Workflow ${webhookId} exists but fires no webhook action. Use list_webhooks to see the ones that do.`
|
|
10243
|
+
});
|
|
10244
|
+
}
|
|
10245
|
+
return jsonResponse(summary);
|
|
10246
|
+
} catch (error) {
|
|
10247
|
+
return errorResponse(error);
|
|
10248
|
+
}
|
|
10072
10249
|
}
|
|
10073
10250
|
);
|
|
10074
|
-
|
|
10075
|
-
server2,
|
|
10251
|
+
server2.tool(
|
|
10076
10252
|
"create_webhook",
|
|
10077
|
-
"Create a
|
|
10253
|
+
"Create a webhook: GHL POSTs to your URL whenever the events you name happen. Builds a real workflow with a `webhook` action, so the client can see and edit it in Automation. Because it rides the workflow layer, it can fire on ANY of GHL's 57 native triggers, not just the short list GHL's documented webhook API covers. Use `customData` to add your own key/value pairs to the body (merge fields like {{contact.first_name}} resolve), and `headers` for auth headers or signatures. Both work on every plan \u2014 GHL's separate 'Custom Webhook' action is a paid add-on and is only needed to replace GHL's contact body wholesale. " + WEBHOOK_PAYLOAD_NOTE + " " + WEBHOOK_FAILURE_NOTE,
|
|
10078
10254
|
{
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
)
|
|
10255
|
+
url: import_zod34.z.string().describe("Destination URL. Must start with http:// or https://."),
|
|
10256
|
+
events: import_zod34.z.array(EventSchema).describe(
|
|
10257
|
+
"Which GHL events fire this webhook. Either a trigger type string ('contact_created') or {type, conditions} for a filtered trigger. Use get_trigger_registry for the 57 types and their fields."
|
|
10258
|
+
),
|
|
10259
|
+
name: import_zod34.z.string().optional().describe("Name shown in the client's workflow list. Defaults to 'Webhook \u2192 <host>'."),
|
|
10260
|
+
method: import_zod34.z.string().optional().describe("POST (default), GET or PUT."),
|
|
10261
|
+
customData: import_zod34.z.array(KeyValueSchema).optional().describe("Extra key/value pairs added to the body under `customData`."),
|
|
10262
|
+
headers: import_zod34.z.array(KeyValueSchema).optional().describe("Extra HTTP headers sent with the request."),
|
|
10263
|
+
publish: import_zod34.z.boolean().optional().describe("Publish immediately so it starts firing. Defaults to true. False leaves it as a draft.")
|
|
10085
10264
|
},
|
|
10086
|
-
async ({
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10265
|
+
async ({ url, events, name, method, customData, headers, publish }) => {
|
|
10266
|
+
try {
|
|
10267
|
+
const client = requireClient();
|
|
10268
|
+
const action = buildWebhookAction({ url, method, customData, headers });
|
|
10269
|
+
const triggers = buildWebhookTriggers(events);
|
|
10270
|
+
let host = url;
|
|
10271
|
+
try {
|
|
10272
|
+
host = new URL(url).host;
|
|
10273
|
+
} catch {
|
|
10094
10274
|
}
|
|
10095
|
-
|
|
10275
|
+
const workflowName = name?.trim() || `Webhook \u2192 ${host}`;
|
|
10276
|
+
const created = await client.createWorkflow(workflowName);
|
|
10277
|
+
const webhookId = readId(created);
|
|
10278
|
+
await client.updateWorkflow(webhookId, {
|
|
10279
|
+
actions: [action],
|
|
10280
|
+
triggers,
|
|
10281
|
+
status: publish === false ? "draft" : "published"
|
|
10282
|
+
});
|
|
10283
|
+
const summary = summarizeWebhookWorkflow(await client.getWorkflow(webhookId));
|
|
10284
|
+
return jsonResponse({
|
|
10285
|
+
created: summary,
|
|
10286
|
+
note: publish === false ? "Created as a DRAFT \u2014 it will not fire until you publish it." : "Published and live. Fire a test event to confirm it reaches your endpoint."
|
|
10287
|
+
});
|
|
10288
|
+
} catch (error) {
|
|
10289
|
+
return errorResponse(error);
|
|
10290
|
+
}
|
|
10096
10291
|
}
|
|
10097
10292
|
);
|
|
10098
|
-
|
|
10099
|
-
server2,
|
|
10293
|
+
server2.tool(
|
|
10100
10294
|
"update_webhook",
|
|
10101
|
-
"
|
|
10295
|
+
"Change a webhook's URL, method, custom data, headers, name, events, or published status. Edits the existing action in place rather than replacing it, so the workflow's history and any other steps around it survive. Only the fields you pass change; everything else is left alone.",
|
|
10102
10296
|
{
|
|
10103
|
-
webhookId: import_zod34.z.string().describe("The webhook ID
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10297
|
+
webhookId: import_zod34.z.string().describe("The webhook ID (its workflow ID)."),
|
|
10298
|
+
url: import_zod34.z.string().optional().describe("New destination URL."),
|
|
10299
|
+
method: import_zod34.z.string().optional().describe("POST, GET or PUT."),
|
|
10300
|
+
customData: import_zod34.z.array(KeyValueSchema).optional().describe("Replaces the existing custom data entirely."),
|
|
10301
|
+
headers: import_zod34.z.array(KeyValueSchema).optional().describe("Replaces the existing headers entirely."),
|
|
10302
|
+
name: import_zod34.z.string().optional().describe("New name for the workflow."),
|
|
10303
|
+
events: import_zod34.z.array(EventSchema).optional().describe("Replaces the triggering events entirely."),
|
|
10304
|
+
status: import_zod34.z.enum(["draft", "published"]).optional().describe("Publish it or take it back to draft (stops it firing)."),
|
|
10305
|
+
actionId: import_zod34.z.string().optional().describe("Required only when the workflow carries more than one webhook action \u2014 which one to edit.")
|
|
10108
10306
|
},
|
|
10109
|
-
async ({ webhookId,
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
10307
|
+
async ({ webhookId, url, method, customData, headers, name, events, status, actionId }) => {
|
|
10308
|
+
try {
|
|
10309
|
+
const client = requireClient();
|
|
10310
|
+
const current = await client.getWorkflow(webhookId);
|
|
10311
|
+
const templates = current.workflowData?.templates ?? [];
|
|
10312
|
+
const webhookActions = templates.filter((a) => a.type === "webhook");
|
|
10313
|
+
if (!webhookActions.length) {
|
|
10314
|
+
throw new Error(`Workflow ${webhookId} fires no webhook action. Use list_webhooks to find one that does.`);
|
|
10315
|
+
}
|
|
10316
|
+
if (webhookActions.length > 1 && !actionId) {
|
|
10317
|
+
throw new Error(
|
|
10318
|
+
`Workflow ${webhookId} has ${webhookActions.length} webhook actions (${webhookActions.map((a) => `${a.id}: ${a.name}`).join(", ")}). Pass 'actionId' to say which one to change.`
|
|
10319
|
+
);
|
|
10320
|
+
}
|
|
10321
|
+
const target = actionId ? webhookActions.find((a) => a.id === actionId) : webhookActions[0];
|
|
10322
|
+
if (!target) throw new Error(`No webhook action with id ${actionId} on workflow ${webhookId}.`);
|
|
10323
|
+
const attributes = target.attributes ?? {};
|
|
10324
|
+
const validated = buildWebhookAction({
|
|
10325
|
+
id: target.id,
|
|
10326
|
+
name: target.name,
|
|
10327
|
+
url: url ?? (typeof attributes.url === "string" ? attributes.url : ""),
|
|
10328
|
+
method: method ?? (typeof attributes.method === "string" ? attributes.method : "POST"),
|
|
10329
|
+
customData: customData ?? readKeyValues(attributes.customData),
|
|
10330
|
+
headers: headers ?? readKeyValues(attributes.headers)
|
|
10331
|
+
});
|
|
10332
|
+
const nextActions = replaceWebhookAttributes(templates, target, validated.attributes);
|
|
10333
|
+
await client.updateWorkflow(webhookId, {
|
|
10334
|
+
actions: nextActions,
|
|
10335
|
+
...events ? { triggers: buildWebhookTriggers(events) } : {},
|
|
10336
|
+
...name ? { name } : {},
|
|
10337
|
+
...status ? { status } : {}
|
|
10338
|
+
});
|
|
10339
|
+
return jsonResponse({ updated: summarizeWebhookWorkflow(await client.getWorkflow(webhookId)) });
|
|
10340
|
+
} catch (error) {
|
|
10341
|
+
return errorResponse(error);
|
|
10342
|
+
}
|
|
10116
10343
|
}
|
|
10117
10344
|
);
|
|
10118
|
-
|
|
10119
|
-
server2,
|
|
10345
|
+
server2.tool(
|
|
10120
10346
|
"delete_webhook",
|
|
10121
|
-
"Delete a webhook
|
|
10347
|
+
"Delete a webhook. This deletes the whole workflow that carries it, so it refuses when that workflow also does other things (sends an SMS, moves an opportunity) unless you pass force. IRREVERSIBLE. To stop a webhook firing without destroying it, use update_webhook with status 'draft'.",
|
|
10122
10348
|
{
|
|
10123
|
-
webhookId: import_zod34.z.string().describe("The webhook ID
|
|
10124
|
-
|
|
10349
|
+
webhookId: import_zod34.z.string().describe("The webhook ID (its workflow ID)."),
|
|
10350
|
+
confirm: import_zod34.z.literal("DELETE").describe("Must be 'DELETE' to confirm this destructive action."),
|
|
10351
|
+
force: import_zod34.z.boolean().optional().describe("Delete even when the workflow contains other actions that would go with it.")
|
|
10125
10352
|
},
|
|
10126
|
-
async ({ webhookId,
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10353
|
+
async ({ webhookId, confirm, force }) => {
|
|
10354
|
+
try {
|
|
10355
|
+
const client = requireClient();
|
|
10356
|
+
if (confirm !== "DELETE") throw new Error("Pass confirm: 'DELETE' to delete a webhook.");
|
|
10357
|
+
const summary = summarizeWebhookWorkflow(await client.getWorkflow(webhookId));
|
|
10358
|
+
if (!summary) {
|
|
10359
|
+
throw new Error(
|
|
10360
|
+
`Workflow ${webhookId} fires no webhook action, so this tool will not delete it. Use delete_workflow_full if you really mean to delete that workflow.`
|
|
10361
|
+
);
|
|
10362
|
+
}
|
|
10363
|
+
if (summary.hasOtherActions && !force) {
|
|
10364
|
+
throw new Error(
|
|
10365
|
+
`Workflow "${summary.name}" does more than fire a webhook \u2014 deleting it would delete those steps too. Pass force: true if that is what you want, or use update_webhook with status 'draft' to just stop it firing.`
|
|
10366
|
+
);
|
|
10367
|
+
}
|
|
10368
|
+
await client.deleteWorkflow(webhookId);
|
|
10369
|
+
return jsonResponse({ deleted: true, webhookId, name: summary.name });
|
|
10370
|
+
} catch (error) {
|
|
10371
|
+
return errorResponse(error);
|
|
10372
|
+
}
|
|
10131
10373
|
}
|
|
10132
10374
|
);
|
|
10133
10375
|
}
|
|
@@ -19096,7 +19338,6 @@ var publicApiTools = [
|
|
|
19096
19338
|
[registerAssociationTools, "associations"],
|
|
19097
19339
|
[registerEstimateTools, "estimates"],
|
|
19098
19340
|
[registerCouponTools, "coupons"],
|
|
19099
|
-
[registerWebhookTools, "webhooks"],
|
|
19100
19341
|
[registerDocumentTools, "documents"],
|
|
19101
19342
|
[registerBulkOperationTools, "bulk-operations"],
|
|
19102
19343
|
[registerTemplateDeployerTools, "template-deployer"],
|
|
@@ -19108,6 +19349,12 @@ var internalApiTools = [
|
|
|
19108
19349
|
[registerFunnelBuilderTools, "funnel-builder"],
|
|
19109
19350
|
[registerPipelineBuilderTools, "pipeline-builder"],
|
|
19110
19351
|
[registerWorkflowClonerTools, "workflow-cloner"],
|
|
19352
|
+
// Webhooks moved here in v3.66.0. GHL has no webhook REST resource — the
|
|
19353
|
+
// old public-API calls 404'd for every customer since 2026-03-23 — so they
|
|
19354
|
+
// are now built on the workflow builder, which needs Firebase auth. The
|
|
19355
|
+
// module registers unconditionally (the tools explain the missing
|
|
19356
|
+
// credentials rather than vanishing), so the advertised tool count holds.
|
|
19357
|
+
[registerWebhookTools, "webhooks"],
|
|
19111
19358
|
[registerSmartListTools, "smart-lists"],
|
|
19112
19359
|
[registerReputationTools, "reputation"],
|
|
19113
19360
|
[registerEmailCampaignTools, "email-campaigns"],
|
package/guide/guide.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>GHL Command — User Guide</title>
|
|
7
|
-
<!-- guides-hash:
|
|
7
|
+
<!-- guides-hash: a156991f1141b0d5 -->
|
|
8
8
|
<style>
|
|
9
9
|
/* Deliberately light in every environment, including a dark-mode OS. This is a
|
|
10
10
|
reference document people read at length, print, and save to PDF, and a page
|
|
@@ -226,7 +226,7 @@ body.app.reading #indexview{display:none}
|
|
|
226
226
|
<p class="eyebrow">GHL Command</p>
|
|
227
227
|
<h1>What do you want to get done?</h1>
|
|
228
228
|
<p class="sub">Every guide is a job, in plain English: open one, copy a prompt, paste it to Claude. A job is not a button, so one guide usually puts a dozen of GHL Command's tools to work at once. Every prompt marked "Proven live" was run against a real GoHighLevel account before it shipped.</p>
|
|
229
|
-
<div class="badges"><span class="badge on">
|
|
229
|
+
<div class="badges"><span class="badge on">28 guides</span><span class="badge">10 categories</span><span class="badge">Updates with the product</span></div>
|
|
230
230
|
</div></section>
|
|
231
231
|
<div class="shell">
|
|
232
232
|
<main>
|
|
@@ -241,12 +241,12 @@ body.app.reading #indexview{display:none}
|
|
|
241
241
|
<a class="card pub" href="#g-ai-bots-and-custom-values" data-k="conversation ai ai bot knowledge base bot goals custom values merge fields voice ai agency name snapshot template personality prompt"><h3>What your AI bot knows, and where to put it</h3><p>Three places a bot reads from, one of which silently ignores your custom values.</p><div class="meta"><span class="pill free">Free plan</span><span class="time">~5 minutes</span><span class="open">Open guide →</span></div></a>
|
|
242
242
|
<a class="card pub" href="#g-audit-your-workflows" data-k="audit workflows broken silent failure check review existing validate not sending nothing happens"><h3>Audit your workflows</h3><p>Find silent failures and dead ends in existing workflows before your client does.</p><div class="meta"><span class="pill free">Free plan</span><span class="time">~2 minutes</span><span class="open">Open guide →</span></div></a>
|
|
243
243
|
<a class="card pub" href="#g-branch-on-tags-and-fields" data-k="if else branch condition vip tag field split two paths different message"><h3>Branch on tags and fields</h3><p>If they are a VIP, send this. Otherwise, send that. Built correctly the first time.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~5 minutes</span><span class="open">Open guide →</span></div></a>
|
|
244
|
-
<a class="card pub" href="#g-build-nurture-sequence" data-k="nurture sequence drip texts emails follow up workflow build quiet hours sending window contact hours"><h3>Build a nurture sequence</h3><p>A follow-up machine that texts, emails, waits, and stops the second someone replies.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~15 minutes</span><span class="open">Open guide →</span></div></a
|
|
244
|
+
<a class="card pub" href="#g-build-nurture-sequence" data-k="nurture sequence drip texts emails follow up workflow build quiet hours sending window contact hours"><h3>Build a nurture sequence</h3><p>A follow-up machine that texts, emails, waits, and stops the second someone replies.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~15 minutes</span><span class="open">Open guide →</span></div></a>
|
|
245
|
+
<a class="card pub" href="#g-send-data-to-other-apps" data-k="webhook zapier make integromat slack google sheets api send data integration notify push connect n8n other app"><h3>Send GHL data to another app</h3><p>Push contacts into Slack, Sheets, Zapier, Make or your own software the moment something happens in GHL.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~2 minutes</span><span class="open">Open guide →</span></div></a></div></div>
|
|
245
246
|
<div class="catsec" id="cat-pipelines-and-sales"><h2>Pipelines and sales</h2><div class="cards"><a class="card pub" href="#g-documents-and-contracts" data-k="documents contracts proposals signature send sign agreement esign template"><h3>Documents and contracts</h3><p>Proposals and contracts sent for signature and tracked from Claude.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~5 minutes</span><span class="open">Open guide →</span></div></a>
|
|
246
247
|
<a class="card pub" href="#g-invoices-and-payments" data-k="invoice bill billing payment paid product price coupon promo code discount transactions orders revenue estimate"><h3>Bill a client and see what you were paid</h3><p>Products, invoices, coupons, and the money reads, without opening the billing screen.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~5 minutes</span><span class="open">Open guide →</span></div></a>
|
|
247
248
|
<a class="card pub" href="#g-stand-up-a-pipeline" data-k="pipeline stages create new client setup opportunity sales process deal stages"><h3>Stand up a pipeline</h3><p>Stages, fields, and tags for a new client in one conversation.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~10 minutes</span><span class="open">Open guide →</span></div></a>
|
|
248
|
-
<a class="card pub" href="#g-track-and-move-opportunities" data-k="opportunities deals move stage track stuck value won lost pipeline board"><h3>Track and move opportunities</h3><p>Move deals between stages, update values, and see what is stuck.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~1 minute</span><span class="open">Open guide →</span></div></a>
|
|
249
|
-
<button class="card locked" type="button" data-k="invoices estimates payments send record coupons products" data-toast="This guide arrives in a product update — your install picks it up automatically." title="Coming in a product update." aria-label="Invoices and estimates. Coming in a product update."><h3>Invoices and estimates</h3><p>Create, send, and track invoices and estimates without opening GHL.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~3 min</span><span class="applabel">Coming in an update</span></div></button></div></div>
|
|
249
|
+
<a class="card pub" href="#g-track-and-move-opportunities" data-k="opportunities deals move stage track stuck value won lost pipeline board"><h3>Track and move opportunities</h3><p>Move deals between stages, update values, and see what is stuck.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~1 minute</span><span class="open">Open guide →</span></div></a></div></div>
|
|
250
250
|
<div class="catsec" id="cat-client-builds-and-pages"><h2>Client builds and pages</h2><div class="cards"><a class="card pub" href="#g-snapshots-and-reusing-a-build" data-k="snapshot template reuse clone account new client share link import rollout onboarding"><h3>Reuse a build across client accounts</h3><p>Find your snapshots, share one, and know exactly what does not travel with it.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~5 minutes</span><span class="open">Open guide →</span></div></a>
|
|
251
251
|
<button class="card locked" type="button" data-k="blueprint client account build intake onboarding new complete template" data-toast="This guide arrives in a product update — your install picks it up automatically." title="Coming in a product update." aria-label="The Blueprint: build a client account. Coming in a product update."><h3>The Blueprint: build a client account</h3><p>From intake form to a complete, reviewed account build you approve before it runs.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~1 hour</span><span class="applabel">Coming in an update</span></div></button>
|
|
252
252
|
<button class="card locked" type="button" data-k="page studio compose website landing pages funnel" data-toast="This guide arrives in a product update — your install picks it up automatically." title="Coming in a product update." aria-label="Page Studio: pages and sites. Coming in a product update."><h3>Page Studio: pages and sites</h3><p>Compose landing pages and full sites that render correctly in GHL.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~20 min</span><span class="applabel">Coming in an update</span></div></button>
|
|
@@ -317,6 +317,18 @@ Do not change anything. This is a read.</p></div>
|
|
|
317
317
|
<div class="gsec"><h2>Where people go wrong</h2>
|
|
318
318
|
<div style="overflow-x:auto"><table class="wrongtbl"><tr><th>What happened</th><th>The fix</th></tr><tr><td>"Workflow notifications go to nobody."</td><td>They point at a user who has gone. List the users, then ask Claude to repoint the notification.</td></tr><tr><td>"Texts fail on a new client account."</td><td>The account has no number, or is not registered for texting yet. The number list shows you in seconds.</td></tr><tr><td>"The export is huge."</td><td>Ask for the part you care about: "just the workflows and pipelines by name".</td></tr><tr><td>"The comparison says the other account is empty."</td><td>Each account's key only works on that account, so the other one could not be read at all. Save its key once and compare again. Claude now says "unavailable" rather than showing zeros, so you can tell the two apart.</td></tr></table></div>
|
|
319
319
|
</div>
|
|
320
|
+
<div class="gsec"><h2>The rest of the account</h2>
|
|
321
|
+
<p class="gpara">One ask covers the things you only look at when something is wrong.</p>
|
|
322
|
+
<div class="prompt"><q>Who has access to this account?</q><button class="copy" type="button">Copy</button></div>
|
|
323
|
+
<div class="prompt"><q>What phone numbers does this account have?</q><button class="copy" type="button">Copy</button></div>
|
|
324
|
+
<div class="prompt"><q>Show me the files in media storage.</q><button class="copy" type="button">Copy</button></div>
|
|
325
|
+
<ul>
|
|
326
|
+
<li><b>Users</b> come back with their name, email, phone and role. This is the fastest way to answer "who can see this client's data", and you need a real user id whenever a workflow has to notify or assign someone.</li>
|
|
327
|
+
<li><b>Phone numbers</b> list with their label, so you can tell which number a client is actually texting from before you wire a workflow to it.</li>
|
|
328
|
+
<li><b>Media</b> is the file library behind your emails and pages. Worth a look when an image stops rendering.</li>
|
|
329
|
+
<li><b>Businesses</b> are GHL's optional grouping layer above contacts. Most accounts have none, and an empty answer here means exactly that, not a failure.</li>
|
|
330
|
+
</ul>
|
|
331
|
+
</div>
|
|
320
332
|
<div class="gsec"><h2>Related guides</h2>
|
|
321
333
|
<ul>
|
|
322
334
|
<li>switch-between-client-accounts</li>
|
|
@@ -634,6 +646,16 @@ When it saves, list the templates in the account so I can see it landed, and tel
|
|
|
634
646
|
<div class="gsec"><h2>Where people go wrong</h2>
|
|
635
647
|
<div style="overflow-x:auto"><table class="wrongtbl"><tr><th>What happened</th><th>The fix</th></tr><tr><td>"The email arrived as one wall of text."</td><td>Say how you want it structured: short paragraphs, one button. Claude will lay it out.</td></tr><tr><td>"The name did not merge."</td><td>The merge field name has to match this account's setup. Ask Claude to check the field exists before saving.</td></tr><tr><td>"I have four versions of the same template."</td><td>Say "update the existing template" instead of describing it again. Repeating the description creates a new one.</td></tr></table></div>
|
|
636
648
|
</div>
|
|
649
|
+
<div class="gsec"><h2>Campaigns, and what is scheduled</h2>
|
|
650
|
+
<p class="gpara">Templates are what an email looks like. Campaigns are the sends built on them.</p>
|
|
651
|
+
<div class="prompt"><q>What email campaigns exist in this account?</q><button class="copy" type="button">Copy</button></div>
|
|
652
|
+
<div class="prompt"><q>Show me every scheduled email send.</q><button class="copy" type="button">Copy</button></div>
|
|
653
|
+
<ul>
|
|
654
|
+
<li>Campaigns come back with their name, subject and how many sends sit under them, which is the quickest way to spot an old sequence still running against a client's list.</li>
|
|
655
|
+
<li>A template is not a send. Editing one does not change anything already scheduled.</li>
|
|
656
|
+
<li>An empty campaign list on a busy account usually means the sends live in workflows instead. Ask for the workflows.</li>
|
|
657
|
+
</ul>
|
|
658
|
+
</div>
|
|
637
659
|
<div class="gsec"><h2>Related guides</h2>
|
|
638
660
|
<ul>
|
|
639
661
|
<li>build-nurture-sequence</li>
|
|
@@ -895,6 +917,16 @@ When it saves, read the form back to me question by question, and tell me exactl
|
|
|
895
917
|
<div class="gsec"><h2>Where people go wrong</h2>
|
|
896
918
|
<div style="overflow-x:auto"><table class="wrongtbl"><tr><th>What happened</th><th>The fix</th></tr><tr><td>"The answers are not on the contact record."</td><td>The questions were not wired to fields. Ask Claude to wire them, and it will create the fields it needs.</td></tr><tr><td>"The form saved but looks wrong on the page."</td><td>The page has its own copy of the form settings. Rebuild the page section with the form after you finish the form, not before.</td></tr><tr><td>"Nobody finished the form."</td><td>Too many questions. Cut it to four and put the rest in the follow-up conversation.</td></tr><tr><td>"It says my test phone number is invalid."</td><td>Numbers starting 555 are reserved for fiction and GHL rejects them. Test with a real-format number instead.</td></tr><tr><td>"I submitted a test and the contact does not come up in search."</td><td>Search runs off an index that lags a few moments behind. The contact record itself is already correct, so open it directly or just ask again shortly.</td></tr></table></div>
|
|
897
919
|
</div>
|
|
920
|
+
<div class="gsec"><h2>Surveys, and the difference from forms</h2>
|
|
921
|
+
<p class="gpara">A form captures somebody in one screen. A survey walks them through questions, often over several pages, and stores its answers separately.</p>
|
|
922
|
+
<div class="prompt"><q>List the surveys in this account.</q><button class="copy" type="button">Copy</button></div>
|
|
923
|
+
<div class="prompt"><q>Show me the last 20 survey responses.</q><button class="copy" type="button">Copy</button></div>
|
|
924
|
+
<ul>
|
|
925
|
+
<li>Ask for surveys explicitly. "Show me my forms" will not include them, and a client asking why their intake answers are missing is usually looking at a survey.</li>
|
|
926
|
+
<li>Both return their answers with the contact attached, so you can go from a response straight to the person.</li>
|
|
927
|
+
<li>Submissions are paged. Ask for a date range or a limit, or you will get the first page and think that is all of them.</li>
|
|
928
|
+
</ul>
|
|
929
|
+
</div>
|
|
898
930
|
<div class="gsec"><h2>Related guides</h2>
|
|
899
931
|
<ul>
|
|
900
932
|
<li>page-studio-pages-and-sites</li>
|
|
@@ -1285,6 +1317,64 @@ Do not reply to anyone. This is a read.</p></div>
|
|
|
1285
1317
|
</main></div>
|
|
1286
1318
|
</section>
|
|
1287
1319
|
|
|
1320
|
+
<section class="gpage" id="g-send-data-to-other-apps">
|
|
1321
|
+
<div class="hero"><div class="hero-in">
|
|
1322
|
+
<p class="eyebrow">GHL Command User Guide</p>
|
|
1323
|
+
<h1>Send GHL data to another app</h1>
|
|
1324
|
+
<p class="sub">Push contacts into Slack, Sheets, Zapier, Make or your own software the moment something happens in GHL.</p>
|
|
1325
|
+
<div class="badges"><span class="badge on">Full license</span><span class="badge">~2 minutes</span><span class="badge">Automations</span></div>
|
|
1326
|
+
</div></div>
|
|
1327
|
+
<div class="shell"><main class="gview">
|
|
1328
|
+
<a class="back" href="#top">← All guides</a>
|
|
1329
|
+
<div class="gsec"><h2>What you'll get</h2>
|
|
1330
|
+
<ul>
|
|
1331
|
+
<li>A live connection that pushes contact details out of GHL into any app that can receive a web request, the moment the thing you care about happens.</li>
|
|
1332
|
+
<li>It can fire on almost anything GHL knows about, not just a new contact: a tag added, a pipeline stage changed, an appointment no showed, a call missed, a form submitted.</li>
|
|
1333
|
+
<li>It shows up in the client's own Automation list, so they can see it, pause it, and edit it without you.</li>
|
|
1334
|
+
</ul>
|
|
1335
|
+
</div>
|
|
1336
|
+
<div class="gsec"><h2>Say this</h2>
|
|
1337
|
+
<div class="prompt"><q>Send every new contact to my Slack channel.</q><button class="copy" type="button">Copy</button></div>
|
|
1338
|
+
<div class="prompt"><q>When a contact moves to Consultation Booked, post them to this address: https://hooks.zapier.com/...</q><button class="copy" type="button">Copy</button></div>
|
|
1339
|
+
<div class="prompt"><q>Show me every webhook in this account and where each one is sending data.</q><button class="copy" type="button">Copy</button></div>
|
|
1340
|
+
</div>
|
|
1341
|
+
<div class="gsec"><h2>The pro prompt</h2>
|
|
1342
|
+
<div class="pro"><div class="pro-head"><b>The exact ask we run on real client accounts</b><span class="pill full">Proven live</span><button class="copy" type="button">Copy</button></div><p class="pro-body">Set up a webhook in my [Radiance Med Spa] account.
|
|
1343
|
+
Fire it whenever a contact gets the tag [hot-lead], and send it to [https://example.com/my-receiver].
|
|
1344
|
+
Add my own fields to what it sends: [clinic] set to [Radiance Med Spa], and [greeting] set to the contact's first name.
|
|
1345
|
+
Then show me exactly what will land at the other end, and tell me what happens if my receiver is down.</p></div>
|
|
1346
|
+
<p class="gpara">Everything in [brackets] is yours to change. Ask for the last part every time. What GHL sends is not what most people expect.</p>
|
|
1347
|
+
</div>
|
|
1348
|
+
<div class="gsec"><h2>What happens when you ask</h2>
|
|
1349
|
+
<ol class="steps">
|
|
1350
|
+
<li>Claude builds a small workflow in the account: your chosen trigger, and one step that sends the data out.</li>
|
|
1351
|
+
<li>It publishes it, so it starts working immediately, and hands you back its ID.</li>
|
|
1352
|
+
<li>Ask it to fire a test, and it will tag a throwaway contact so you can watch the request land before you trust it with real leads.</li>
|
|
1353
|
+
</ol>
|
|
1354
|
+
</div>
|
|
1355
|
+
<div class="gsec"><h2>Good to know</h2>
|
|
1356
|
+
<ul>
|
|
1357
|
+
<li><b>What arrives is a fixed set of contact fields</b>, plus the sub-account details, plus which workflow sent it, plus any custom fields you asked for. Your own fields can include merge fields, so "the contact's first name" arrives already filled in.</li>
|
|
1358
|
+
<li><b><code>tags</code> arrives as one comma separated line of text, not a list.</b> This catches people out constantly. If the receiving app checks whether tags contains "vip", it will also match a contact tagged "vip-waitlist". Split on the comma first.</li>
|
|
1359
|
+
<li><b>If the receiving app is down, GHL keeps trying</b>, roughly five minutes later, then ten minutes after that. Nothing is thrown away.</li>
|
|
1360
|
+
<li><b>But the contact stops there while it retries, and so does everything after it in that workflow.</b> If the send is not critical, put it at the end of the workflow, or give it a workflow of its own, so an outage at the other end cannot hold up your follow up.</li>
|
|
1361
|
+
<li>GHL has a paid "Custom Webhook" step that lets you replace the whole message with your own format. You almost never need it. Adding your own fields, your own headers, and choosing POST, GET or PUT all work on every plan.</li>
|
|
1362
|
+
</ul>
|
|
1363
|
+
</div>
|
|
1364
|
+
<div class="gsec"><h2>Where people go wrong</h2>
|
|
1365
|
+
<div style="overflow-x:auto"><table class="wrongtbl"><tr><th>What happened</th><th>The fix</th></tr><tr><td>"I deleted the webhook and it deleted my whole nurture."</td><td>It will refuse to do that. A webhook here is a workflow, so if that workflow does other things too, Claude stops and tells you instead of deleting it. To just switch a webhook off, ask to set it back to draft.</td></tr><tr><td>"My receiver only got some of the leads."</td><td>Something earlier in the workflow is holding contacts up, or the send is behind a step that failed. Ask Claude to audit that workflow.</td></tr><tr><td>"My code crashed on <code>tags.map</code>."</td><td><code>tags</code> is text, not a list. Split it on commas first.</td></tr><tr><td>"I pointed it at a test address and forgot."</td><td>Ask for every webhook in the account and where each one sends. Old test addresses are the most common thing this turns up.</td></tr></table></div>
|
|
1366
|
+
</div>
|
|
1367
|
+
<div class="gsec"><h2>Related guides</h2>
|
|
1368
|
+
<ul>
|
|
1369
|
+
<li>audit-your-workflows</li>
|
|
1370
|
+
<li>build-nurture-sequence</li>
|
|
1371
|
+
<li>branch-on-tags-and-fields</li>
|
|
1372
|
+
</ul>
|
|
1373
|
+
</div>
|
|
1374
|
+
<p class="gfoot">Works in Claude Desktop and Claude Code. Stuck? Email support@ghlcommand.com and a human answers.</p>
|
|
1375
|
+
</main></div>
|
|
1376
|
+
</section>
|
|
1377
|
+
|
|
1288
1378
|
<section class="gpage" id="g-send-texts-and-emails">
|
|
1289
1379
|
<div class="hero"><div class="hero-in">
|
|
1290
1380
|
<p class="eyebrow">GHL Command User Guide</p>
|
|
@@ -1331,6 +1421,14 @@ Before you send: confirm the exact contact you matched, show me their phone numb
|
|
|
1331
1421
|
<div class="gsec"><h2>Where people go wrong</h2>
|
|
1332
1422
|
<div style="overflow-x:auto"><table class="wrongtbl"><tr><th>What happened</th><th>The fix</th></tr><tr><td>"The text says sent but never arrived."</td><td>The sub-account is not fully registered for texting. Check A2P status in GHL. This is the most common cause by far.</td></tr><tr><td>"It went to the wrong person."</td><td>Two contacts share a first name. Always give the full name or email and read the match Claude shows you.</td></tr><tr><td>"I meant to send twenty of these."</td><td>Twenty one-off sends is a sequence in disguise. Build a nurture; it stops on reply and keeps you out of trouble.</td></tr></table></div>
|
|
1333
1423
|
</div>
|
|
1424
|
+
<div class="gsec"><h2>Bulk campaigns</h2>
|
|
1425
|
+
<p class="gpara">Individual messages are one thing; a campaign is the older bulk-send layer sitting alongside your workflows.</p>
|
|
1426
|
+
<div class="prompt"><q>What campaigns are running in this account?</q><button class="copy" type="button">Copy</button></div>
|
|
1427
|
+
<ul>
|
|
1428
|
+
<li>Most modern accounts have none, because workflows replaced them. An empty answer is normal, not broken.</li>
|
|
1429
|
+
<li>If a client swears they are still emailing people and you cannot find the workflow, check here before you go hunting.</li>
|
|
1430
|
+
</ul>
|
|
1431
|
+
</div>
|
|
1334
1432
|
<div class="gsec"><h2>Related guides</h2>
|
|
1335
1433
|
<ul>
|
|
1336
1434
|
<li>review-your-conversations</li>
|
|
@@ -1505,6 +1603,14 @@ Then confirm back to me, one line each, that every post saved as a draft and whi
|
|
|
1505
1603
|
<div class="gsec"><h2>Where people go wrong</h2>
|
|
1506
1604
|
<div style="overflow-x:auto"><table class="wrongtbl"><tr><th>What happened</th><th>The fix</th></tr><tr><td>"The posts went nowhere."</td><td>The account connection had expired. Reconnect it in GHL, then the queue drains normally.</td></tr><tr><td>"The captions sound like a robot."</td><td>Give Claude two real posts from the client and say "write like these". Voice comes from examples, not adjectives.</td></tr><tr><td>"It posted to the wrong page."</td><td>Name the exact page or profile in the ask, and check the account list Claude prints first.</td></tr></table></div>
|
|
1507
1605
|
</div>
|
|
1606
|
+
<div class="gsec"><h2>Which accounts are actually connected</h2>
|
|
1607
|
+
<p class="gpara">Before scheduling anything, check what the account can post to.</p>
|
|
1608
|
+
<div class="prompt"><q>Which social accounts are connected here?</q><button class="copy" type="button">Copy</button></div>
|
|
1609
|
+
<ul>
|
|
1610
|
+
<li>Connected accounts come back grouped, so you can see at a glance whether the client connected a page, a profile, or nothing at all.</li>
|
|
1611
|
+
<li>Scheduling to a platform that was never connected fails quietly at post time, long after you have moved on. Check first on any account you did not connect yourself.</li>
|
|
1612
|
+
</ul>
|
|
1613
|
+
</div>
|
|
1508
1614
|
<div class="gsec"><h2>Related guides</h2>
|
|
1509
1615
|
<ul>
|
|
1510
1616
|
<li>blog-posting</li>
|
|
@@ -1790,6 +1896,13 @@ Tell me plainly what happens on my side when somebody clicks it, and what I woul
|
|
|
1790
1896
|
<div class="gsec"><h2>Where people go wrong</h2>
|
|
1791
1897
|
<div style="overflow-x:auto"><table class="wrongtbl"><tr><th>What happened</th><th>The fix</th></tr><tr><td>"No reviews came back and I know we have some."</td><td>Reviews live per sub-account. Confirm which account you are in, then ask again. Reviews also only appear here once the platform they came from is connected under Reputation.</td></tr><tr><td>"It says no reviews, but there are some on our Facebook page."</td><td>Having a review link for a platform is not the same as having that platform connected. Until it is connected, the reviews stay on Facebook and never reach the account.</td></tr><tr><td>"I want alerts, not to keep asking."</td><td>Ask for a workflow instead: a review request after every completed appointment, and a notification to you on anything low.</td></tr><tr><td>"The reply Claude wrote sounds defensive."</td><td>Tell it the tone: short, human, take responsibility, offer to fix it privately. It will rewrite.</td></tr></table></div>
|
|
1792
1898
|
</div>
|
|
1899
|
+
<div class="gsec"><h2>Review links, and where the requests come from</h2>
|
|
1900
|
+
<div class="prompt"><q>What is my review link?</q><button class="copy" type="button">Copy</button></div>
|
|
1901
|
+
<ul>
|
|
1902
|
+
<li>The review link is the URL your request messages point customers at. Read it back before you paste it into a workflow, because a wrong one sends people nowhere and nobody reports it.</li>
|
|
1903
|
+
<li>Reviews come back with the rating, the text, the reviewer and the source, so you can answer "what are people actually saying" in one ask rather than reading a dashboard.</li>
|
|
1904
|
+
</ul>
|
|
1905
|
+
</div>
|
|
1793
1906
|
<div class="gsec"><h2>Related guides</h2>
|
|
1794
1907
|
<ul>
|
|
1795
1908
|
<li>weekly-account-report</li>
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.66.0",
|
|
4
4
|
"mcpName": "io.github.drjerryrelth/ghl-command",
|
|
5
|
-
"description": "GoHighLevel MCP Server for Claude. 238 tools
|
|
5
|
+
"description": "GoHighLevel MCP Server for Claude. 238 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"ghl-mcp": "dist/index.js"
|