> ## 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.

# Subscriber opportunity sync

# Funding Machine subscriber opportunity sync

## Target and activation boundary

Source agency: `TY8NPnDY2A1uP5yZ8YKJ`. Destination: **My Funding Machine**,
`ReoQmuh4ooNuHeE5stlj`. These are pinned in code. All mirror tables are keyed by
the destination `locationId`; source identity is a separate `sourceLocationId`.
Only internal Convex maintenance functions and a disabled cron can access them.
There is deliberately no browser API, operator-facing roster endpoint, or health
dashboard in V1.

Implementation approval covers the additive schema, scheduler, and code. This
does **not** activate production. Deployment, production owner confirmations,
pipeline/field creation, and enabling live writes need the named production
authorization. Do not use a credit pull or a payment write to validate this sync.

Read-only inspection on September 25, 2026 found:

* Destination OAuth works for location, pipeline, field, opportunity, and payment
  subscription reads. Use its own installation token, not the agency token.
* Existing pipelines: `3. FM Client Onboarding`, `Onboarding`, and `Retention`.
  None is a dedicated subscriber pipeline. Leave them unchanged.
* Multiple opportunities per contact are already enabled.
* The eight sync fields below do not exist yet.
* The refreshed agency list contained 220 records, 95 Active (the earlier export
  had 93). Source counts are observations, not a fixed enrollment manifest.

No live configuration or subscriber records were written during implementation.

## Destination configuration

After the production configuration checkpoint, create **Funding Machine
Subscribers** with exactly these stage names: **Active**, **Trialing**, **Past
Due**, **Paused**, **Canceled**. Use the CRM pipeline UI; the installed official
SDK exposes pipeline reads but no pipeline creation method. Runtime lookup
resolves the unique pipeline and stage IDs within the pinned destination.

Create these **opportunity** custom fields in that destination. Exact names and
types are validated on every run. Do not reuse the existing `Product Location ID`
field from other workflows.

| Field name | CRM type | Meaning |
| - | - | - |
| FM Subscriber Source Location ID | TEXT | Immutable source business identity |
| FM Subscriber Plan | TEXT | Current SaaS plan title |
| FM Subscriber Amount | NUMERICAL | Recurring catalog amount, USD, before discounts/taxes/setup fees |
| FM Subscriber Billing Frequency | TEXT | monthly or yearly; annual amounts are not divided by twelve |
| FM Subscriber Next Billing Date | TEXT | ISO date or `No renewal scheduled`; unknown preserves prior value |
| FM Subscriber User Count | NUMERICAL | Count of current, assigned account users |
| FM Subscriber Last Sync Time | TEXT | ISO time of last fully successful sync |
| FM Subscriber Billing Contact | TEXT | Billing contact name/email/phone, separate from main owner |

The settings check requires `allowDuplicateOpportunity=true` because one owner
may have multiple businesses. It does not change that setting automatically.
Review existing workflow triggers for these new records before activation. The
sync never sends an email/message or enrolls a contact in a workflow itself.

## Read-only preview and owner confirmation

Run commands against the intended deployment. Commands below omit `--prod` on
purpose. Only add it for explicitly authorized production operations. All new
functions are internal; deploying them is a separate step from checking out code.

```sh theme={null}
bunx convex run admin/subscriberSyncNode:preview '{}'
bunx convex run admin/subscriberSyncNode:preview '{"sourceLocationIds":["SOURCE_LOCATION_ID"]}'
```

The first command discovers the whole agency and configuration. The second adds
live details for up to ten chosen source IDs: roster, owner, contact matches,
existing opportunity, proposed owned fields, and exceptions. Repeat batches for
the intended rollout. Preview is entirely read-only, including OAuth: it will
report a stale/missing agency or destination credential instead of rotating it.
Normal existing token-refresh operations can restore those prerequisites.

After a human identifies the main owner, record the **user** ID from that source
roster, plus an attributable confirmation note. Never guess from the first admin
or equate the billing contact with the main owner. Confirmation is blocked while
a run is active so it cannot race a worker.

```sh theme={null}
bunx convex run admin/subscriberSync:confirmOwner '{"sourceLocationId":"SOURCE_LOCATION_ID","userId":"CONFIRMED_SOURCE_USER_ID","note":"Confirmed by NAME on DATE from SOURCE"}'
```

Pass `userId:null` with a new note to revoke a confirmation. Re-run the preview
after confirmations. Main-owner changes that need a different destination
primary contact remain exceptions until staff relink the same opportunity.
The documented Update Opportunity API has no `contactId` update field.

## Enable and inspect

After reviewing the preview and obtaining production activation approval, set
`SUBSCRIBER_SYNC_ENABLED=true` in the intended Convex deployment. This setting is
not a Next.js setting. Missing or any other value keeps the scheduler disabled.

```sh theme={null}
bunx convex run admin/subscriberSyncNode:start '{"dryRun":false}'
bunx convex run admin/subscriberSync:recentRuns '{}'
bunx convex run admin/subscriberSync:state '{}'
bunx convex run admin/subscriberSync:roster '{"sourceLocationId":"SOURCE_LOCATION_ID"}'
```

`start` defaults to preview unless `dryRun:false` is explicit, and live mode still
requires the deployment switch. `recentRuns` reports cursor/queue length and
synced/needsReview/skipped counts. `state` shows mapped opportunity IDs, last
attempt, last full sync, and per-account issues. `roster` includes its observation
time. Restrict these outputs to staff because they contain customer information.

First observe an authorized run and inspect destination readbacks before
describing the automation as live. New enrollment is Active-only. Once managed,
all five supported stages reconcile, including reactivation. Opportunity
lifecycle status is preserved; new opportunities start `open`.

The next scheduled discovery runs every six hours. A crashed worker's run can
resume through `start` after its 15-minute lease expires, which exceeds Convex's
10-minute action lifetime. Concurrent starts reuse the one active destination
run. Only one worker owns it at a time; creates have durable intents written
before the external request.

## Failure handling and recovery

* Incomplete/changing source pagination stops discovery before queuing writes.
  Discovery failures are visible in Convex logs. Unknown source statuses and
  missing listings are per-account exceptions, never inferred cancellations.
* Watchlist IDs `q3gEsVFOc126Rln8a3FL`, `Exq0dCONAQ7BcjcFzFTs`, and
  `lVHjDVx6cf3pi9R30VNd` were visible in CRM Active but absent from the earlier API
  export. Discovery retains them even before they have a mapping. If they appear
  later, normal source checks apply. Clear Trac's Setup Pending is not proof of
  an active billable subscription.
* A failed roster read preserves the last roster and count. A missing owner
  blocks new opportunity creation. For an existing opportunity, known stage/name
  and billing fields may update while owner review remains visible.
* Exact `priceId` matching is required. V2 prices are dollars; V1 prices are cents.
  Missing units, unknown currency/interval, or historical prices absent from the
  catalog remain exceptions. No guessed fallback price is written.
* The SaaS API does not provide billing dates/contact snapshots. V1 reads the
  exact payment subscription under the destination `altId`, verifies its ID and
  tenant, and reads its billing contact separately. Unavailable payment details
  preserve existing fields, keep the last full sync timestamp unchanged, and
  flag the account. The next date is the payment subscription period end unless
  it is scheduled not to renew. Unsupported payment shapes remain exceptions.
* A missing/moved/mismatched mapped opportunity is never recreated or moved
  back automatically. Duplicate source IDs or duplicate exact-email contacts
  stop that account for review. A provider search failure is never an empty set.
* Create timeout/crash recovery searches the full destination inventory for the
  source ID, including other pipelines, and adopts only a unique matching
  opportunity in the subscriber pipeline. Pending contact creates similarly
  reconcile by exact destination email. If absent, the durable intent stays
  blocked. Inspect provider records/logs before a separately authorized manual
  repair; there is deliberately no blind retry/reset command.
* Primary-contact changes are a manual relink on the existing opportunity.
  Updating a source owner's email does not silently overwrite a destination
  contact or create a replacement opportunity.
* Only sync-owned custom-field IDs are sent on updates. Notes, assignments,
  status, followers, monetary value, and unrelated fields are omitted.

## Disable and resume

Set `SUBSCRIBER_SYNC_ENABLED=false` to stop new workers. An in-flight worker may
finish. A scheduled next worker closes the run as stopped. For an abandoned run,
after its lease expires:

```sh theme={null}
bunx convex run admin/subscriberSync:stop '{}'
```

This does not delete opportunities, contacts, mappings, rosters, or create intents.
To resume, restore the switch after reviewing a new preview, then call `start`
with `dryRun:false`. Identity and unresolved-create safeguards survive stop/resume.

## Primary API references

* [Search users](https://marketplace.gohighlevel.com/docs/ghl/users/search-users/)
* [Opportunity update contract](https://marketplace.gohighlevel.com/docs/2023-02-21/ghl/opportunities/update-opportunity/)
* [Opportunity search](https://marketplace.gohighlevel.com/docs/ghl/opportunities/search-opportunity/)

The installed official SDK declarations and live read-only payloads were also
checked for pagination, SaaS plan units, destination settings, and payment
subscription response shape. No live create/update endpoint was exercised.


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