AcquisitionPRO® / CRM Integration

AcquisitionPRO® is the WALDO-bundled AcquisitionPRO® or your CRM deployment. If your CRM is AcquisitionPRO® or your CRM directly (not under the AcquisitionPRO® brand), this all still applies — same API, same configuration, same behavior.

The AcquisitionPRO® / CRM integration pushes a note + structured data to specific contacts in your CRM whenever a WALDO analysis completes. There are two ways to use it, and you can use both at once:

  1. Standing rules (
    text
    /settings/integrations
    → AcquisitionPRO® / CRM section) — set up once, fires forever. "Whenever any analysis on these markets finishes, post a note on this contact." Great for keeping a personal CRM trail or auto-updating a retainer client's record.
  2. Per-export ad-hoc (Phase 6 Results dashboard → "Send to webhook / email / CRM" tile) — pick contacts at send time. "Send this specific analysis to broker X, client Y, and our internal ops contact." Great for consultants juggling multiple clients where the recipients change deal by deal.

TL;DR for CRE professionals: Stop manually telling clients "your analysis is ready" and stop copy-pasting numbers into CRM notes. Either set up a standing rule (auto-fires forever) OR pick contacts on the Phase 6 export tile (ad-hoc). Both push a clean note to the contact's timeline with the market name, completion timestamp, and a quick summary block — and update a structured custom field automations can read.


What you get from this

  • Automatic note timeline. Every analysis that matches a mapping creates a note on the contact in AcquisitionPRO® or your CRM. Your team's history with that contact stays current without anyone copy-pasting.
  • Structured data field. A custom field
    text
    waldo_latest_analysis
    on the contact gets a JSON snapshot of the latest analysis (market, completion date, top metrics). AcquisitionPRO® or your CRM automations can trigger off this — send an SMS, kick off a workflow, branch on the value.
  • Per-mapping market filter. Don't push every analysis to every contact. Map "Acquisition lead — ACME Holdings" to only Cuyahoga + Hamilton county FIPS codes; only those analyses fire notes on that contact.
  • Audit log. Every push attempt is logged with the contact, the analysis ID, success/failure, and any error message. You see at a glance whether AcquisitionPRO® or your CRM is keeping up.
  • Pause and resume. Each mapping has its own active/paused toggle. The settings page also has a "Pause all" / "Resume all" bulk button.

Real-world workflows

MappingWhat it does
Map your primary contact (the broker) to all markets (no filter)Every completed analysis you run posts a note on the broker's record. Useful for solo operators who want a personal CRM trail.
Map a specific client contact (e.g. "Smith Family Office") to the markets you've pitched themOnly analyses on their target markets create notes on their record. Other analyses don't pollute their timeline.
Map an internal "Ops Inbox" contact to all marketsYour ops team sees every analysis-completed note in one place; the broker's contacts only see their relevant ones.
Map a competitor-tracking contact to specific MSAsNotes accrue automatically as you run quarterly updates on the markets that competitor cares about.

Step-by-step: setting up a standing rule

  1. Sign in as a Professional or Enterprise user. AcquisitionPRO® / AcquisitionPRO® or your CRM must already be provisioned for your WALDO account (admin handles this — if you're on AcquisitionPRO® it's already configured).
  2. Go to Settings → Integrations.
  3. Find the AcquisitionPRO® / CRM section (below Scheduled Exports).
  4. Click "Add mapping."
  5. Search by name or email — the autocomplete queries your AcquisitionPRO® or your CRM location for matching contacts. Pick the one you want.
  6. Set a label (e.g. Acquisition lead — ACME Holdings). This is for your reference in the WALDO settings page; it doesn't affect what AcquisitionPRO® or your CRM sees.
  7. Optionally, add a market filter. Comma-separated FIPS codes (
    text
    39035, C26900
    ). Leave blank to push every completed analysis.
  8. Click "Add mapping." Done.
  9. Test it by completing an analysis on one of the filtered markets (or any market if no filter). Within ~2 minutes the events-outbox subscriber will pick up the analysis-completed event and post a note + custom-field update. Watch the Recent pushes panel below the mappings list — you'll see the row with status "Succeeded" (green) or "Failed" (red).

Step-by-step: ad-hoc per-export send

For consultants serving multiple clients (where the recipients differ deal by deal), skip standing rules and use the export tile instead:

  1. Open any completed analysis in WALDO and go to the Results & Reporting tab (Phase 6).
  2. Click the Export sub-tab.
  3. Click the amber "Send to webhook / email / CRM" tile.
  4. In the modal:
    • Pick format (PDF / Excel / PowerPoint).
    • Optionally pick one or more registered webhook endpoints.
    • Optionally enter email recipients (your account email is pre-filled).
    • Search for AcquisitionPRO® / CRM contacts in the multi-select chip field. Type a name or email; matching contacts appear; click to add. Each picked contact appears as a removable chip. Add as many as the deal needs.
  5. Click Send. WALDO generates the export once and fans out: emails go out, webhooks fire, AND a note + custom-field update is pushed to each picked CRM contact.

The ad-hoc path uses the same

text
addAnalysisNote
+
text
pushAnalysisSummary
calls as standing rules — same note format, same custom field, same audit trail (entries appear in the standing-rules push log too with
text
mapping_id = null
so you can tell the two paths apart).


What gets written to the contact

For each mapping that fires, WALDO writes:

Note (POST

text
/contacts/{id}/notes
):

text
📊 <your org name> analysis completed: Cuyahoga County

Market type: county (39035)
Completed: Wed, 30 Apr 2026 13:00:00 GMT
Top LQ sector: NAICS 622 @ LQ 1.34
Net shift-share: +1,245 jobs
Supply gap: -125 units

Analysis ID: 5fa9c0a2-...

The note title uses your org's white-label

text
companyName
for parity with exports + emails.

Custom field (PUT

text
/contacts/{id}
with
text
customFields: [{ key: "waldo_latest_analysis", field_value: "..." }]
):

json
{
  "source": "<your org name>",
  "analysis_id": "5fa9c0a2-...",
  "market_name": "Cuyahoga County",
  "market_type": "county",
  "market_code": "39035",
  "completed_at": "2026-04-30T13:00:00Z",
  "top_lq": { "naics": "622", "lq": 1.34 },
  "net_shift_share": 1245,
  "supply_gap": -125
}

The custom field is best-effort — if your AcquisitionPRO® or your CRM location doesn't have a

text
waldo_latest_analysis
custom field provisioned, the field write is silently ignored. The note write is the load-bearing path, so the integration still works without the field. You only need to set the field up if you want AcquisitionPRO® or your CRM automations to read structured analysis data (e.g. send an SMS when an analysis with a specific market_code lands).

How to add the
text
waldo_latest_analysis
custom field in AcquisitionPRO® (AcquisitionPRO® or your CRM)

You'll do this once per AcquisitionPRO® or your CRM location — about two minutes.

  1. Sign in to AcquisitionPRO® / AcquisitionPRO® or your CRM as a user with permission to edit settings (sub-account admin or agency admin).
  2. In the left sidebar, click the gear icon (Settings) at the bottom.
  3. In the settings menu, click Custom Fields.
  4. Make sure the Contact tab is selected at the top (custom fields can be scoped to Contact, Opportunity, etc. — we want Contact).
  5. Click + Add Field (top right).
  6. Pick the field type Multi Line / Large Text. (Single Line works too but caps at ~255 chars; Large Text is safer because the JSON payload can run 200–600 chars on busy analyses.)
  7. Fill in:
    • Name (label):
      text
      WALDO Latest Analysis
    • Field Key:
      text
      waldo_latest_analysis
      (this exact value — lowercase, underscores, no spaces — is what WALDO writes to)
    • Group / Folder: optionally create a "WALDO" group to keep it organized
    • Leave Required, Show in Forms, and Placeholder at their defaults
  8. Click Save.
  9. (Optional but useful) Open any contact in AcquisitionPRO® or your CRM and confirm the new field appears in the contact's profile sidebar. It will be empty until WALDO pushes its first update.

That's it. The next time an analysis completes that matches one of your mappings, both the note AND the structured custom field will populate. You can wire AcquisitionPRO® or your CRM automations off

text
Contact.WALDO Latest Analysis
from the Automations builder using the
text
Field Updated
trigger or by reading the field in conditional steps.

Verifying it worked: after a push, open the contact in AcquisitionPRO® or your CRM → scroll the right sidebar to the WALDO group → you should see a JSON object pasted into the field. If the field stays blank but a new note still posted on the contact's timeline, the most likely cause is a misspelled field key (it must be exactly

text
waldo_latest_analysis
).


Pause, resume, delete

  • Pause one mapping — that mapping stops firing; others keep working.
  • Pause all — bulk toggle every mapping inactive in one click. Useful for going on vacation or pausing during data investigations.
  • Delete — removes the mapping. Past push history is preserved on
    text
    highlevel_push_log
    .

Tier requirements

AcquisitionPRO® or your CRM push is included on Professional and Enterprise plans. Push activity does not consume API quota — it's an internal subscriber, not a user-facing API call.

The integration uses the platform-level AcquisitionPRO® or your CRM API key set in WALDO's environment, not a per-user OAuth token. All your contact pushes go to the same AcquisitionPRO® or your CRM location WALDO is configured against. If you operate multiple AcquisitionPRO® or your CRM locations and need per-user routing, contact support.


For your integrator (or AI agent)

Everything below is the technical reference. Paste a link to this page into Claude / ChatGPT / Cursor and ask the AI to wire up the integration — it has all the information it needs.

Architecture

  • WALDO has a single
    text
    HIGHLEVEL_API_KEY
    +
    text
    HIGHLEVEL_LOCATION_ID
    set at the deployment level (env vars). All pushes go through this single key against this single location. There is no per-user OAuth.
  • A user adds rows to
    text
    highlevel_contact_mappings
    via the settings UI. Each mapping pairs a AcquisitionPRO® or your CRM
    text
    contact_id
    with an optional market filter (
    text
    market_codes TEXT[]
    ).
  • Vercel Cron
    text
    /api/internal/jobs/process-highlevel-events
    runs every 2 minutes. It reads
    text
    events_outbox
    rows where
    text
    event_name='analysis.completed'
    AND
    text
    hl_processed_at IS NULL
    , finds matching mappings for each event's user, and pushes a note + custom-field update via the AcquisitionPRO® or your CRM API.
  • Each push attempt is recorded on
    text
    highlevel_push_log
    (success or failure). The event row's
    text
    hl_processed_at
    is marked regardless of push outcome — failed pushes don't infinitely retry. Users can re-run a failed push manually by deleting + recreating the mapping (or by triggering a new analysis on the same market).

Schema

text
highlevel_contact_mappings
:

  • text
    id, user_id, hl_contact_id, hl_contact_email, hl_contact_label, market_codes TEXT[], active, created_at, updated_at

text
highlevel_push_log
:

  • text
    id, user_id, mapping_id, analysis_id, event_id, hl_contact_id, hl_contact_email, status, error_message, pushed_at

text
events_outbox
(extension):

  • Added column
    text
    hl_processed_at TIMESTAMP WITH TIME ZONE
    — independent dimension from
    text
    processed_at
    (which the webhook fanout uses) so the AcquisitionPRO® or your CRM subscriber and webhook processor don't race.

Library API

text
lib/integrations/highlevel-api.ts
exports:

  • text
    searchContact(query: string): Promise<AcquisitionPRO® or your CRMSearchHit[]>
    — autocomplete by name or email.
  • text
    addAnalysisNote(contactId, summary, brandingName): Promise<{ success, error? }>
    — POST
    text
    /contacts/{id}/notes
    .
  • text
    pushAnalysisSummary(contactId, summary, brandingName): Promise<{ success, error? }>
    — PUT
    text
    /contacts/{id}
    updating the
    text
    waldo_latest_analysis
    custom field.

text
AnalysisSummaryForHL
shape:

ts
{
  analysisId: string
  marketName: string
  marketType: string
  marketCode: string | null
  completedAt: string | null
  topLocationQuotient?: { naics: string; lq: number } | null
  netShiftShare?: number | null
  supplyGap?: number | null
}

Internal management API (Supabase JWT auth)

MethodPathDescription
GET
text
/api/settings/highlevel-mappings
List caller's mappings
POST
text
/api/settings/highlevel-mappings
Create mapping
GET
text
/api/settings/highlevel-mappings/:id
Fetch one
PATCH
text
/api/settings/highlevel-mappings/:id
Update label, market_codes, active
DELETE
text
/api/settings/highlevel-mappings/:id
Delete
GET
text
/api/settings/highlevel-mappings/contact-search?q=...
Autocomplete proxy (HL API key never exposed to browser)
GET
text
/api/settings/highlevel-push-log?limit=50
Recent pushes

Cron details

text
*/2 * * * *
(every 2 minutes). Auth:
text
Authorization: Bearer ${CRON_SECRET}
. Method: GET. Returns
text
{ processed, pushed, failed }
. Skips cleanly with
text
{ skipped: 'AcquisitionPRO® or your CRM not configured' }
if env vars are unset.

Failure modes + retry semantics

  • text
    HIGHLEVEL_API_KEY
    unset
    — the cron returns early with no work done. No mappings fire until env is set.
  • HL API returns 4xx/5xx — push is logged as
    text
    failed
    with the response body. Event is still marked
    text
    hl_processed_at
    — no infinite retry. User can recreate the mapping or trigger a new analysis to retry.
  • Custom field write fails (e.g., field not provisioned) — note write succeeds, status is
    text
    succeeded
    , error_message includes
    text
    field: ...
    . Note is the load-bearing path.
  • Mapping market filter excludes the analysis — no push, no log row. Silent.

Privacy + data flow

The only data sent to AcquisitionPRO® or your CRM:

  • The contact ID (already known to AcquisitionPRO® or your CRM).
  • A note body containing market name, FIPS code, completion timestamp, optional top-line metrics, and the WALDO analysis ID.
  • A JSON-stringified summary written to the
    text
    waldo_latest_analysis
    custom field.

No PII from your client list crosses to AcquisitionPRO® or your CRM. Mappings are user-scoped; one user's mappings cannot push to another user's contacts.