The URL
POST https://app.hiambrose.com/api/team/<webhook_id>/webhook/ghl
The webhook_id is an immutable 8-character alphanumeric assigned at team creation. Find it on the team's Integrations tab. Slug-based URLs (/api/team/<slug>/webhook/ghl) also work for back-compat.
/webhook/ghl is gone. Any GHL workflow still pointing at /api/agent/<anything>/webhook/ghl returns 404 with a migration message. Move to the team URL.Request type & parameters
| Field | Type | Required |
|---|---|---|
| Method | POST | yes |
| Content-Type | application/json | yes |
GHL's Webhook action sends the contact object plus a customData map (this is where you put your instruction + flags). Ambrose reads these fields:
contact_id) and an instruction (customData.instructions). Without an instruction the team falls back to a generic review/draft; for GHL workflows always send one.| Body field | Required | What it is |
|---|---|---|
contact_id / contact.id / top-level id | Yes | The GHL contact. Resolution order: contact_id → customData.contact_id → contact.id → top-level id → email lookup. (Nested workflow.id / location.id are never used as the contact.) |
customData.instructions (or customData.instruction) | Yes | The command for the team — e.g. "Draft a renewal SMS and add it as a note." |
email / contact.email | If no id | Used to look up the contact when no id is present. Loads that contact's prior conversation, notes, fields & opportunities. |
customData.sync | No | true to wait for the result in the HTTP response (use when a later workflow step needs the output). Default is fast-ack: Ambrose returns immediately and processes in the background. |
customData.auto_create_fields | No | true to let the team create missing custom fields. Off by default. |
| any other contact fields | No | name, phone, tags, and every contact custom field — all passed to the team as context. |
In the GHL Webhook action you typically map {{contact.id}} and add a customData key instructions with your command. The instruction is read only from customData.instructions (or customData.instruction) — a top-level instruction is ignored. (Top-level contact_id is accepted for non-GHL callers like curl / Make / Zapier.)
- Full team persona — every team markdown file (SOUL / PERSONALITY / FLOW / NOTES / FAQ …) as the system prompt.
- The skills you selected on the team's Tools tab — each skill's playbook is injected so the AI follows it.
- The GHL CRM toolset — read conversation/notes/fields/pipelines/appointments + write notes/tags/fields, draft & send SMS/email, move pipeline stages, create tasks.
- The complete inbound payload — every contact custom field GHL sends (both the top-level fields like
ai_context_json,Contact Context JSON, business/AEO/social fields, AND anything undercustomData) is passed to the AI as context. Empty fields are skipped to keep the prompt tight. - The contact's prior conversation history — pulled live from GHL by contact id (oldest→newest), so replies are in-context.
The AI then performs the task in customData.instructions using all of the above.
Example body
GHL sends the contact fields at the top level and your task inside customData:
{
"contact_id": "{{contact.id}}",
"email": "{{contact.email}}",
"full_name": "{{contact.name}}",
"location": { "id": "{{location.id}}" },
"ai_context_json": "…", // GHL also sends every contact custom field at the top level
"customData": {
"instructions": "Draft a renewal SMS in our voice, add it as a note, and tag 'renewal-drafted'.",
"sync": false
}
}
How it works end to end
ghl spoke.tier, note, tag_add, tag_remove, sms_draft, …) so the GHL workflow can pick them up as variables.The two operating modes
Mode 1 — Default (review & draft)
If you do not provide an explicit instruction, the team reads the contact and returns a triage: a tier + a one-line reason + suggested next actions. Nothing is written to GHL unless the team prompt explicitly says so.
Mode 2 — Operator instruction
If the webhook body includes customData.instructions, the team treats it as a direct command. The instruction-router has ~20 tools available:
| Read | Write |
|---|---|
| read_conversation | add_note |
| read_notes | add_tags |
| read_opportunities | remove_tags |
| read_custom_fields_schema | update_contact_native |
| read_pipelines | update_custom_fields |
| read_tags | create_custom_field (off by default) |
| read_appointments | generate_sms / send_sms |
| read_calendars | generate_email / send_email |
| move_pipeline_stage | |
| create_task |
Example 1 — Lead Qualifier
Trigger in GHL: New contact added to "Web leads" tag.
Workflow webhook body sent to Ambrose:
{
"contact_id": "abc123",
"location_id": "loc_xxx",
"first_name": "Maria",
"last_name": "Lopez",
"email": "maria@example.com",
"phone": "+12145551234",
"custom_fields": { "interest": "ICHRA", "budget": "350" }
}
Team prompt (the Lead Qualifier from the prompts page) returns:
{
"tier": "HOT",
"reason": "ICHRA inquiry + budget > $300/mo with explicit week-out timeline.",
"actions": [
{ "type": "add_tags", "tags": ["hot_lead", "ichra"] },
{ "type": "add_note", "body": "Qualified HOT by Ambrose — ICHRA + budget $350." },
{ "type": "create_task", "title": "Jordan: book ICHRA discovery", "due_in_hours": 24 }
]
}
The handler flattens tier to a top-level field, executes the actions, and the GHL workflow's next step branches on {{webhook_response.tier}} == "HOT".
Example 2 — Operator instruction
Trigger: Tag added: "renewal_pending". Workflow webhook body includes an instruction:
{
"contact_id": "xyz789",
"customData": {
"instructions": "Draft a renewal email in our brand voice. Include current plan, last year's claim count, and propose a 15-min review call. Add the draft as a contact note and tag 'renewal_drafted'."
}
}
The team uses the instruction-router to: read_conversation + read_notes + generate_email + add_note + add_tags. No SMS or email actually sends — the draft is parked as a note for human review.
Example 3 — Calendar booked → send confirmation
{
"contact_id": "lmn456",
"appointment": {
"calendar_id": "cal_xyz",
"start_time": "2026-05-29T15:00:00Z"
},
"customData": {
"instructions": "Send a calendar-confirmation SMS in my brand voice. Include the time in their local zone, link to a 2-question prep form, and a reminder to test their phone audio. Stop here."
}
}
Uses: generate_sms → send_sms. add_note with the sent body.
Response: fast-ack vs sync
By default the webhook is fast-ack: it validates the payload, queues the job, and returns immediately (so GHL's 30-second step timeout never trips). The AI runs in the background and writes its results straight into GHL (notes, tags, custom fields). If a later workflow step needs to branch on the AI's output, send customData.sync: true — then the call waits and returns the full result below (capped by GHL's 30s timeout; use only for low-volume flows).
In sync mode the response exposes:
{
"ok": true | false,
"tier": "...", // if your prompt returned one
"actions_taken": [...], // every write that ran
"custom_fields_updated": [...],
"errors": [...], // any write that failed
"raw": { ... } // the team's full output
}
In fast-ack (default) mode the response is a small acknowledgement — read outcomes from the contact's updated fields/notes, or check the team's Logs tab.
Safety rails
- Webhooks are open by design — GHL does not let you set arbitrary auth headers. Use URL secrecy (the unguessable webhook_id) and signature verification if you need stronger protection.
- Auto-create-off by default — the team cannot create new custom fields unless you flip a per-team flag.
- Outbound SMS / email defaults to draft mode. Switch a team to auto-send only after you have watched it for a few days.
- Every webhook fire is logged in the team's Logs tab with the inbound body, tool calls, and response.
Connecting in GHL
{{contact.*}} variables, and add a customData key instructions with your command.HIPAA
The ghl spoke is scrubbed by default. Per-sub-account you can flip a BAA flag in the GHL integration card (only if you have a signed BAA covering that sub-account's data).