> ## Documentation Index
> Fetch the complete documentation index at: https://docs.myfundingmachine.com/llms.txt
> Use this file to discover all available pages before exploring further.

# New lead marketplace action

# New Lead marketplace workflow action (FM-2606)

Internal runbook. Operator-facing steps live in
`docs/operators/getting-started/new-lead-workflow-action.mdx`.

## What it is

The marketplace app's existing draft workflow action `create_fm_applicant`
("Create Funding Machine Applicant", templateId `01KM6NZ3ERBWMSCMT231E6VTWG`) is
repointed so its execution URL is `POST /api/marketplace/new-lead`. That draft
pre-dated FM-2606 with a custom payload mirroring the webhook body and no execution
URL; it was never published. Reusing it avoids a second near-duplicate action. It is the drop-in
replacement for the workflow "Custom Webhook" step that posts to
`POST /api/webhooks/new-lead`.

Both routes are thin adapters over the ONE intake + applicant-gate + auto-credit-pull
implementation in `lib/webhooks/new-lead-intake.ts` (`processNewLeadIntake`). They
differ only in transport:

| | Webhook step (`/api/webhooks/new-lead`) | Marketplace action (`/api/marketplace/new-lead`) |
| - | - | - |
| Configured by | Each operator, in each workflow | Us, once, in the marketplace app |
| Auth | `Authorization: Bearer <WEBHOOK_NEW_LEAD_SECRET>` or HMAC header | `Authorization: Bearer <GHL_NEW_LEAD_ACTION_TOKEN>` (static header on the action) |
| Tenant | `locationId` in the body the operator built | `extras.locationId`, filled by the CRM from the executing sub-account |
| Contact | `contactId` in the body | `extras.contactId` |
| Optional identity/attribution | body fields | action inputs with the same names (`contactName`, `contactEmail`, `contactPhone`, `companyName`, `source`, `dubId`, `referralOrgId`); empty strings are treated as absent |
| Address, tags, custom fields, score | body or CRM contact enrichment | CRM contact enrichment (same code path a webhook body without them already takes) |

Execution payload the CRM sends:

```json theme={null}
{
  "data": { "contactName": "", "source": "" },
  "extras": { "locationId": "...", "contactId": "...", "workflowId": "..." },
  "meta": { "key": "create_fm_applicant", "version": "1.0" }
}
```

Response (same shape as the webhook route): `{ "success": true, "leadEventId": "...", "duplicate": false }`.
`leadEventId` and `duplicate` are exposed to later workflow steps as custom variables.

### Why dual execution cannot double pull or double charge

Idempotency is not in either adapter; it is in the shared implementation and the
Convex mutations it calls, so it holds regardless of which ingress fires:

* `leadEvents.logLeadEvent` collapses a second delivery inside its dedupe window, or
  while an applicant-opportunity claim is unresolved, into the same lead event.
* `leadEvents.claimLeadEventOpportunity` lets exactly one caller create the applicant
  opportunity for a lead event.
* `autoCreditPullEvents.reserveTrigger` lets exactly one caller reserve the auto pull
  per `(locationId, contactId)`, and any prior decision short-circuits before the
  wallet or bureau is touched. Outside the lead-event window a second ingress still
  gets its own lead event and application (one application per submission is the
  existing policy; the CRM gives the action no submission id to key on, and inventing
  one would change the webhook's semantics too) but it cannot pull. Operator docs
  therefore say: back to back is fine, never separate the two steps with a Wait.
* `/api/credit-report/new` keeps its own wallet/charge checks unchanged.

`tests/newLeadApplicantPerSubmission.test.ts` ("FM-2606" block) drives the webhook
route and the action route against the same in-memory Convex state and asserts one
application and one bureau POST in both the inside-window and outside-window cases.

## Where it is in the developer portal

Workflow actions are app-scoped and are not in the app **version** dropdown (v7.0.1 /
v6.0.0). In the developer portal open App Settings for the app, then
`modules/workflows` → Actions. Direct link to the draft config (Brock, 2026-09-17):

`https://marketplace.gohighlevel.com/app-settings/697a568ce94ccaee772a84b0/version/6a74dfe1a56f7909daa13f72/modules/workflows/actions/69bdcaa344f5e92d32354e65/config?version=1.0&tempId=01KM6NZ3ERBWMSCMT231E6VTWG`

The action is "Create Funding Machine Applicant" (id `01KM6NZ3ERBWMSCMT231E6VTWG`). The
portal's "Test" button and `ghl app actions test` both execute the real endpoint; do
not use them against production until `GHL_NEW_LEAD_ACTION_TOKEN` is set and a test
sub-account and contact have been agreed, because a successful run writes a lead event
and can trigger a billable pull.

## Environment

* `GHL_NEW_LEAD_ACTION_TOKEN`: static bearer the action sends. Generated with
  `openssl rand -hex 32`. Set on Vercel (project `ghl-marketplace-app-fundingmachine`,
  team My Figures) on Production, Preview, and Development as a Sensitive variable on
  2026-09-17 with Brock's approval; the portal draft header carries the same value.
  Existing deployments do not see a new variable until they redeploy; merging this PR
  deploys production and picks it up.
* The proxy exempts the exact path `/api/marketplace/new-lead` (see
  `lib/proxy/isExemptPath.ts`); the route verifies the bearer itself.

## Marketplace configuration with the official CLI

Use the official `@gohighlevel/marketplace-cli` in isolation (not `~/.local/bin/ghl`).
Run everything from a scratch directory outside the repo and pin the version.

```bash theme={null}
export GHL_CONFIG_DIR=/Users/fm-mini/tmp/cos-launch/fm-2606-ghl-cli/config   # isolated credentials
alias ghl='bunx @gohighlevel/marketplace-cli@1.0.0'

ghl login --no-browser              # prints an approval URL; a human must open it within 5 minutes
ghl account --json                  # confirm the Funding Machine developer account is active
ghl app list --json                 # identify the Funding Machine app id
ghl app pull <appId> --directory /Users/fm-mini/tmp/cos-launch/fm-2606-ghl-cli --folder fm-app --json
ghl app actions --directory /Users/fm-mini/tmp/cos-launch/fm-2606-ghl-cli/fm-app --json   # existing actions
ghl app scopes --directory /Users/fm-mini/tmp/cos-launch/fm-2606-ghl-cli/fm-app            # must include workflows.readonly

cp docs/internal/marketplace-actions/create-fm-applicant.json \
   /Users/fm-mini/tmp/cos-launch/fm-2606-ghl-cli/fm-app/src/modules/workflows/actions/create-fm-applicant.json

ghl app actions validate --directory /Users/fm-mini/tmp/cos-launch/fm-2606-ghl-cli/fm-app
ghl app actions diff     --directory /Users/fm-mini/tmp/cos-launch/fm-2606-ghl-cli/fm-app --json
GHL_NEW_LEAD_ACTION_AUTHORIZATION="Bearer <GHL_NEW_LEAD_ACTION_TOKEN>" \
  ghl app actions push   --directory /Users/fm-mini/tmp/cos-launch/fm-2606-ghl-cli/fm-app --json   # creates the DRAFT only
ghl app actions          --directory /Users/fm-mini/tmp/cos-launch/fm-2606-ghl-cli/fm-app --json   # verify remote draft
```

Rules while doing this:

* Only the `ghl app actions ...` sub-commands. Never `ghl app push`: it owns the app
  profile, OAuth, and the shared marketplace webhook (`src/webhooks/ghl-webhooks.json`),
  none of which this task touches.
* `ghl app actions publish` is HELD. Publishing makes the action visible to every
  installed sub-account and needs its own approval. Publish only after this PR has
  merged and deployed, otherwise executions hit a production proxy that has no route.
* If `workflows.readonly` is missing from the app's scopes, stop. Adding a scope is
  Sensitive Scope (auth/scopes) and forces a major version bump on the next app
  publish; it needs separate approval.
* The header value `${env:GHL_NEW_LEAD_ACTION_AUTHORIZATION}` is resolved at push
  time from the shell and never written to disk or to git. After a pull the portal
  value is exported as `${remote}`.

### Status from the 2026-09-17 run

* Logged in as the Funding Machine developer account (isolated `GHL_CONFIG_DIR`).
* App identified: "My Funding Machine", appId `697a568ce94ccaee772a84b0`, live version
  `7.0.1` (versionId `6a74dfe1a56f7909daa13f72`). `workflows.readonly` is already in
  `oauth.allowedScopes`; no scope change needed.
* Existing remote actions before the change: `create_fm_applicant` (draft 1.0, custom
  payload mirroring the webhook body, no execution URL, created 2026-03-20) and
  `fm_admin_processing_form_submitted` (draft 1.0, untouched).
* `ghl app actions push` applied exactly one operation, `update:create_fm_applicant@1.0`,
  and the post-push `ghl app actions diff` is empty. The action is still a DRAFT
  (`isActive: false`); nothing is visible to sub-accounts until `publish`, which is held.
* The previous draft definition is preserved at
  `/Users/fm-mini/tmp/cos-launch/fm-2606-ghl-cli/create-fm-applicant.PREVIOUS-DRAFT.json`.
* The bearer was generated with `openssl rand -hex 32` into the 0600 file
  `/Users/fm-mini/tmp/cos-launch/fm-2606-ghl-cli/secrets/GHL_NEW_LEAD_ACTION_TOKEN` and
  injected at push time, then set as `GHL_NEW_LEAD_ACTION_TOKEN` on Vercel (see
  Environment above). It was never printed or committed.
* `ghl app pull` for the full app stops at billing validation because the portal's
  existing usage meters fail the CLI's local rules (`pricingPageUrl`, `minPricePerUnit`).
  App, webhook, and action files still pull. Not this task's problem; do not "fix" the
  meters from here.
* Not run: `ghl app push` (app profile / shared webhook untouched), `ghl app actions
  publish`, `ghl app actions test` (would execute a real intake against production).

### CLI verification on 2026-10-06

* The pinned official CLI `1.0.0` successfully pulled the full app with
  `--with-types` after the billing configuration repair documented in
  `marketplace-meters.md`. The September 17 billing-validation blocker is resolved.
* `create_fm_applicant@1.0` remains a draft. Local action validation, including
  `--publishable --action create_fm_applicant --version 1.0`, passes. Its pulled
  definition matches the repository definition apart from the generated `$schema`
  reference and the redacted `${remote}` authorization placeholder.
* Schema and declaration generation succeeds. An action-only pull and diff showed
  no drift or conflicts. Local validation does not establish runtime correctness
  or authorize publication.
* No action was executed, pushed, or published during this verification. Continue
  using action-scoped commands for action changes; a successful full pull does not
  authorize a whole-app push.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.