# Voice AI Agents API (verified live 2026-08-07)

Base: `https://services.leadconnectorhq.com` — header `Version: 2021-07-28` required. GHL docs map (JS-rendered; use the sitemap to find pages): `marketplace.gohighlevel.com/docs/ghl/voice-ai/*` (agents, create-agent, get-agent, patch-agent, delete-agent, actions, create-action, update-action, delete-action, get-call-logs, dashboard, voice-ai-api).

## Auth: location token ONLY

The V3 agency token (168 scopes incl. `voice-ai-agents.write`) gets **401 `authClass type is not allowed`** on ALL `/voice-ai/*` endpoints. A location-level token (`userType: Location`) is required — same restriction class as funnels. RRR's location token was minted 2026-08-07 via the direct chooselocation link with an explicit `scope=` list (see SKILL.md pitfalls #19/#20). RRR locationId: `4yTvHUHrmVuP8CgCt1Q9`.

## Endpoints (all verified 2026-08-07)

- `GET /voice-ai/agents?locationId=<id>` — list agents
- `GET /voice-ai/agents/{agentId}?locationId=<id>` — full agent incl. actions array
- `POST /voice-ai/agents` — create; `locationId` in BODY works here
- `PATCH /voice-ai/agents/{agentId}?locationId=<id>` — update; **`locationId` MUST be a query param** (in body → 403 "LocationId is required")
- `POST /voice-ai/actions?locationId=<id>` — add action; body: `{locationId, agentId, actionType, name, actionParameters}` → 201
- `DELETE /voice-ai/actions/{actionId}?locationId=<id>&agentId=<agentId>` — **BOTH query params required** (missing agentId → 403 "AgentId is required"); success = 204 empty

## Agent schema fields observed (GET single agent)

`agentName, businessName, welcomeMessage, agentPrompt, voiceId, language, responsiveness (0.7), maxCallDuration (600 = seconds), sendUserIdleReminders, reminderAfterIdleTimeSeconds, timezone, inboundNumbers[], callEndWorkflowIds[], sendPostCallNotificationTo{admins,allUsers,contactAssignedUser,specificUsers[],customEmails[]}, agentWorkingHours[], isAgentAsBackupDisabled, toolCallStrictMode, translation{enabled}, actions[]`

## Gotchas

- **Action names: letters/numbers/spaces/underscores/hyphens ONLY** — an em-dash in `name` → 400 "Action name can only contain letters, numbers, spaces, underscores, and hyphens".
- **Attach a phone number:** PATCH `{"inboundNumbers": ["+1XXXXXXXXXX"]}` (full digits, E.164). Verified: shows in GHL UI under Phone numbers → Configure → "Calls Go To → Voice AI (1st Priority)". The singular `inboundNumber` field stayed null after this — read `inboundNumbers`, don't rely on the singular.
- **SMS action has NO destination field.** `actionType: SMS` texts the CONTACT (the caller) — there is no way to hardcode a third-party recipient in a voice-ai action. For "text the owner after every call", use a workflow (below). Confirmed against Connie - RRR's existing SMS actions (same shape, no `to`).
- **`callEndWorkflowIds` on the agent** = native per-agent workflow wiring (fires without any trigger filters). Connie - RRR has 3 workflow UUIDs attached this way. Settable via PATCH once the workflow ID is known (workflow ID is in the GHL workflow-builder URL). Not yet exercised by us — we used a filtered "Call details" trigger instead.
- **voiceId is copyable across agents.** Connie - RRR uses `g6xIsTj2HwM6VR4iXFCw`; reuse it for voice consistency across RRR agents.
- **Location-token scope-subset trap:** if the location install URL's `scope=` list omits e.g. `locations/customFields.write` or `workflows.readonly`, those endpoints 401 "The token is not authorized for this scope" under the location token even though the APP has them — the token carries only what the install URL requested. Re-run the install link with a wider `scope=` to fix (multi-token handler overwrites that location's token, others untouched).
- **Existing RRR voice agents (as of 2026-08-07):** "Connie - RRR" (`6a0f6a221c9ebcf26dbe92ee`, on the 720 number, 6KB prompt, CALL_TRANSFER + APPOINTMENT_BOOKING + DATA_EXTRACTION + SMS actions), "Connie - Pipeline Layer" + a copy (stranded in RRR, like the 877-LAN-EXIT number), 2× "18 Local Biz Live Demo", "AI Sells Itself" (third-party template), "Missed Call Only Agent (Bail Bonds)" (`6a75d6414ee3e02404bb0127`, on 877-LAN-EXIT, built this session). Always list before creating.

## Prerequisites for call transcripts (workflow trigger warned until both on)

1. Sub-account: **Phone System → Voice → Call Recording & Transcription → Call Transcription** toggle ON.
2. Per phone number: **Phone numbers → <number> → Configure → Call Recording** toggle ON (was already on for 877-LAN-EXIT, with a "This call will be recorded for quality purposes" disclosure message — keep it; consent posture).

## Workflow triggers for voice-agent events

- **"Transcript generated"** (Communication group) — **PREMIUM trigger**: GHL warns "Using this trigger will incur additional charges per execution". Fires after transcription completes (30–60s after hangup). Filter UI exposes only `Type = Voice AI/IVR` plus contact custom fields — **NO agent-name or phone-number filter fields**. Blocked with an orange warning until transcription is enabled. Don't use this if a non-premium alternative works.
- **"Call details"** (Events group) — **standard, non-premium**. Fires after a call ends with a chosen status: `completed / busy / canceled / no-answer / voicemail`. For "agent finished a call": status=`completed` (the others mean the agent never talked). Call-native filters: **Direction** (incoming/outgoing), **In phone number** (pick the specific number), In number pool, Custom disposition. Correct recipe for the bail-bonds handoff: status=completed + Direction=incoming + In phone number=877-526-3948.
- **Filter-dropdown trap:** the trigger filter field picker dumps ALL contact custom fields into the same list (RRR has dozens of survey-style fields — "Are you currently generating a steady flow of new leads…", etc.). Call-native fields are at the top under "Standard fields". Don't get lost in the custom-field dump; if the field you need isn't there, that trigger doesn't support it.
- **The in-product AI workflow builder hallucinates filters:** it claimed to add an agent-name filter that "Transcript generated" doesn't support. Always open the trigger after the AI builds and verify the actual filter row (field/operator/value). Still worth using — Rob pastes an agent-written prompt into the builder and it scaffolds trigger+action faster than fighting the UI blind (his suggestion; worked well). Verify, then fix.
- **Workflow action recipient trap (burned 2026-08-07):** the workflow "Send SMS" action ("Sends a text message to the contact") has NO To: field — it always texts the contact, and `{{contact.phone}}` in the body is a display variable, not a destination. The AI builder picks it by default, which would have texted the bail-bonds caller their own handoff summary. For "text Rob's cell after a call", use the **"Internal Notification" / "Send Internal SMS"** action type instead (accepts an arbitrary hardcoded number), or `callEndWorkflowIds` wiring + internal notification. Verify the action's recipient model before publishing ANY AI-built workflow.

## "No SMS arrived" diagnostic (burned 2026-08-07)

When the workflow fires and the agent's call completed but no SMS landed on the notification number, diagnose from the workflow's **Execution log** tab (top of the workflow builder — NOT Enrollment history; that's enrollments, not action outcomes). Four observed shapes:

1. **"Finished"** (every action marked green) → no SMS: **contact tags / disposition blocked it.** The contact record has a workflow-blocking disposition (e.g. **"Spam Call"** — Rob's bail-bonds test contact had this; "DND", "do not message", "test contact" behave the same). Fix: open the contact, remove the offending tag, OR test with a fresh phone that's NOT an existing contact in GHL. For production this is **good** behavior — TCPA-safe. For demos it surfaces unexpectedly when the test caller is a known contact.
   - **GHL auto-tags first-time inbound callers as Disposition=Spam Call sometimes** — this is per-phone-number (carrier risk scoring), NOT per-contact-record. Deleting the contact and re-calling from the same number re-applies the disposition on the freshly-created contact. Workaround: use a different phone (Google Voice, friend's cell, prospect's phone) for demos. The disposition eventually clears on its own after the number makes a few non-flagged calls.
2. **"Skipped"** with reason **"All the custom phone numbers provided are invalid"** — TWO sub-causes:
   - **Field format wrong:** Internal Notification's **Custom Number** is empty or in an unparseable format. The field silently accepts and shows common US formats (`303-828-7753`, `+13038287753`) but execution still reports them invalid. **Always re-open the action after saving and read the live value** — saves that LOOK clean still trigger this skip. Fix: re-enter as digits-only E.164 (`+13038287753`), Save, then verify by re-opening the action and confirming the field shows the formatted number.
   - **TCPA STOP-list (new — verified 2026-08-07):** the destination number is on a per-sub-account unsubscribe list, often inherited from prior account ownership or a previous SMS subscriber. **Verify BEFORE relying on the workflow** — go to Conversations → New conversation → enter the number → send a manual "test" SMS. If the manual send returns **"Cannot send message as +1XXX has unsubscribed"**, the number is permanently STOP-listed for this sub-account. GHL Support can clear them but it's slow (requires documented opt-in date). For demos: pick a different destination number, or use the Email notification variant (case 4) instead.
3. **"Failed"** (rare, not hit live): usually an SMS-credits or carrier-billing issue on the sub-account — verify Settings → Phone System → Messaging has credits / is not suspended.
4. **Internal Notification → Email validation error** ("There are issues in your custom variables, please fix them before saving"): GHL's internal-email action rejects ALL `{{contact.*}}` and `{{call.*}}` merge tags in the body — it has a much smaller variable scope than the regular "Send Email" action. Workaround: use plain text only, or switch to a regular "Send Email" action that supports merge tags.

**Diagnostic order:** Execution log → click the Skipped/Failed row → click "View details" → read "Skipped Due To" / failure reason. Don't re-run the test call until the previous run's failure is fixed; the log accumulates per execution.

## Rob's numbers for notification wiring

- Real cell (demo handoffs, "standing in front of the prospect showing my phone"): **303-828-7753** — TCPA STOP-listed for RRR sub-account 2026-08-07 (verified: manual SMS rejected with "has unsubscribed"). Use Email or a different number for demos until cleared.
- Google Voice: +1 303-***-7628 (masked by API) — Connie's CALL_TRANSFER target. VoIP — likely ALSO blocked by SMS validator. Don't conflate with the real cell.
- Rule for any new voice-agent demo: **send a manual test SMS to the destination number FIRST** (Conversations → New → enter number → send). If it bounces, that number is STOP-listed for the sub-account and the workflow can never deliver to it.