Skip to content

CRM Integration

Use the CRM Integration workflow to connect HubSpot, Salesforce, or any other CRM to GraphADV. Upload firm records from your system of record and maintain a durable link to GraphADV’s canonical firm IDs, so you can push updates back to your CRM and pull GraphADV insights without manual matching. The /account/firms/* endpoints replace one-off /firms/search calls with a fully auditable integration surface behind the GraphADV API proxy.

  1. Deterministic resolution – SEC number, CRD number, and exact domain matches auto-confirm (candidate snapshots only include entries when fuzzy scoring ran).
  2. Candidate generation – Name, domain, and geography similarity return up to five candidates.
  3. Trigram scoring – Candidates are ranked with deterministic heuristics plus trigram-based fuzzy matching.
  4. Policy gate – High-confidence matches auto-confirm; everything else is stored as ambiguous.
  5. Durable persistence – Each outcome is persisted and exposed via /account/firms along with timestamps, review reasons, candidate snapshots, and method metadata.

Resolver runs are tracked via the run_id returned from /account/firms/resolve, allowing GTM teams to replay a given ingestion or audit how many firms were auto-confirmed vs. surfaced for review. Locked records are skipped without any database writes, so mixed batches can include previously confirmed firms without fear of overwriting links.

EndpointMethodPurposeExample Payload
/account/firms/resolvePOSTBatch resolve CRM payloads and return linked, ambiguous, unmatched, or locked statuses[{"external_firm_id":"crm_123","name":"Example Capital","domain":"examplecapital.com"}]
/account/firms?status=ambiguousGETList firms that still require manual confirmation
/account/firms/confirmPOSTConfirm an ambiguous match (optionally overriding the candidate){"external_firm_id":"crm_456","graphadv_firm_id":"c3fa21da-2d8e-4d2d-9a4c-9d4a5a6d1d40"}
/account/firms/overridePOSTOverride the ambiguous match with a user-selected GraphADV firm{"external_firm_id":"crm_789","graphadv_firm_id":"9a0f0ae8-04c5-40b1-80d4-3e7cd8a2a780","reason":"Parent RIA confirmed"}
/account/firms/rejectPOSTClear the stored link for a CRM record so it re-enters the resolve queue{"external_firm_id":"crm_987","reason":"Duplicate location"}
/account/firms/unlockPOSTUnlock a previously linked firm (requires reason){"external_firm_id":"crm_987","reason":"Need to reprocess with updated CRM data"}
  • linked – Deterministic or high-confidence fuzzy match was auto-confirmed. Candidate snapshots include scored candidates when fuzzy logic ran; deterministic matches may return an empty list.
  • ambiguous – Needs human review. Includes candidates and reasons for explainability.
  • unmatched – No viable candidates. Continue editing the CRM payload or rerun after enrichment.
  • locked – Record was previously confirmed and marked immutable. Resolver skipped it entirely; call the /account/firms/unlock with a reason before re-running resolve.

Locked links ensure the “never re-resolve” principle: once a reviewer confirms a mapping, /account/firms/resolve refuses to overwrite it and responds with status: "locked" plus the existing candidate snapshot (if any fuzzy candidates were generated previously). Unlock by calling /account/firms/unlock with the external_firm_id and a reason, which writes an audit log before allowing another resolve attempt.

Push any CRM export—HubSpot companies, Salesforce accounts, Microsoft Dynamics accounts, etc.—into /account/firms/resolve to register the GraphADV link and begin syncing insights back to your system.

[
{
"external_firm_id": "crm_123", // required
"name": "Example Capital Management",
"domain": "examplecapital.com",
"sec_number": "801-55555",
"crd_number": "123456",
"address": "123 Main Street, New York, NY",
"city": "New York",
"state": "NY",
"country": "US",
"notes": "RIA based in NYC",
"source": "salesforce"
}
]

Response:

{
"run_id": "be7c25c6-70bf-4f16-9ad6-0dca2cd4db19",
"results": [
{
"external_firm_id": "crm_123",
"external_firm_name": "Example Capital Management",
"fuzzy_name_lookup": "example capital management",
"status": "linked", // linked | ambiguous | unmatched | locked
"graphadv_firm_id": "43f8ad30-ef86-4c4b-a080-01b9bf67f4b8",
"confidence": 0.97,
"method": "domain", // sec_number | crd | domain | fuzzy | ...
"reasons": [
"Exact domain match"
],
"candidates": [
{
"firm_id": "43f8ad30-ef86-4c4b-a080-01b9bf67f4b8",
"legal_name": "EXAMPLE CAPITAL MANAGEMENT LLC",
"score": 0.97,
"reasons": ["Exact domain match"],
"city": "New York",
"state": "NY",
"country": "United States",
"domain": "examplecapital.com",
"aum_bucket": "$10B",
"employee_bucket": "100+"
}
]
}
]
}

Once confirmed, downstream GTM tools can call /account/firms/{external_firm_id}/profile and reuse the GraphADV schema without juggling SEC/CRD/domain matching logic themselves.

  1. Upload CRM records via POST /account/firms/resolve. The proxy runs deterministic ID checks, domain matching, and a fuzzy name scorer immediately, then responds with per-record decisions plus up to five ranked candidates.
  2. Fetch the review queue using GET /account/firms?status=ambiguous. Each row includes the original payload (candidate_snapshot.input) and the scored candidates (candidate_snapshot.candidates).
  3. Approve or override directly from your automation:
    • POST /account/firms/confirm to accept the stored candidate.
    • POST /account/firms/override with your own graphadv_firm_id if you prefer a different firm.
    • POST /account/firms/reject to clear a bad match.
  4. Automate at scale by applying your own confidence thresholds—e.g., auto-confirm any ambiguous match with score ≥0.95—and calling the confirm/override endpoints in bulk. Every change is immediately reflected by /account/firms, so downstream /profile|signals|principals lookups work as soon as a record is confirmed.
Terminal window
curl -X GET "$BASE_URL/account/firms?status=ambiguous&limit=2" \
-H "x-api-key: $GRAPHADV_API_KEY"

Response:

[
{
"external_firm_id": "crm_456",
"status": "ambiguous",
"candidates": [
{"graphadv_firm_id": "c3fa21da-2d8e-4d2d-9a4c-9d4a5a6d1d40", "score": 0.88, "reasons": ["Domain overlap", "City match"]},
{"graphadv_firm_id": "d9bb6a01-5697-45f7-b3df-2a23793c0118", "score": 0.74, "reasons": ["Name similarity 0.82"]}
],
"candidate_snapshot": {
"input": {"name": "Blue Rock Capital", "domain": "bluerockcapital.co"},
"candidates": [
{"graphadv_firm_id": "c3fa21da-2d8e-4d2d-9a4c-9d4a5a6d1d40", "score": 0.88},
{"graphadv_firm_id": "d9bb6a01-5697-45f7-b3df-2a23793c0118", "score": 0.74}
]
},
"last_reviewed_at": null
}
]
Terminal window
curl -X POST "$BASE_URL/account/firms/confirm" \
-H "x-api-key: $GRAPHADV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_firm_id": "crm_456",
"graphadv_firm_id": "c3fa21da-2d8e-4d2d-9a4c-9d4a5a6d1d40",
"review_notes": "Auto-confirmed above 0.9"
}'

Response:

{
"external_firm_id": "crm_456",
"status": "linked",
"graphadv_firm_id": "c3fa21da-2d8e-4d2d-9a4c-9d4a5a6d1d40",
"reviewed_by": "automation@customer.com",
"reviewed_at": "2025-01-12T15:02:55Z",
"notes": "Auto-confirmed above 0.9"
}
Terminal window
curl -X POST "$BASE_URL/account/firms/override" \
-H "x-api-key: $GRAPHADV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_firm_id": "crm_789",
"graphadv_firm_id": "9a0f0ae8-04c5-40b1-80d4-3e7cd8a2a780",
"reason": "Account team confirmed entity is the parent RIA"
}'

Response:

{
"external_firm_id": "crm_789",
"status": "linked",
"graphadv_firm_id": "9a0f0ae8-04c5-40b1-80d4-3e7cd8a2a780",
"method": "manual_override",
"reason": "Account team confirmed entity is the parent RIA"
}
Terminal window
curl -X POST "$BASE_URL/account/firms/reject" \
-H "x-api-key: $GRAPHADV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_firm_id": "crm_987",
"reason": "Record is a duplicate location"
}'

Response:

{
"external_firm_id": "crm_987",
"status": "unmatched",
"reason": "Record is a duplicate location"
}

With these endpoints you can script a full loop: resolve batches, poll for ambiguous rows, confirm or override based on your thresholds, and fall back to manual review only when necessary.

When customers send CRM exports with inconsistent field names, run them through the built-in AccountFirmRecordMapper to coerce records into the resolver’s schema. The mapper instructs OpenAI to normalize IDs/domains and stores any unused keys in unmapped_fields so operations teams keep full fidelity.

from src.services.account_firm_mapper import AccountFirmRecordMapper
raw_rows = [
{"crm_id": "A-123", "company": "Example Capital", "website": "https://examplecapital.com"},
{"account_guid": "B-999", "Firm Name": "Grey Rock Partners", "custom": "Original payload"},
]
mapper = AccountFirmRecordMapper()
mapped = mapper.map_records(raw_rows)
for record in mapped:
payload = record.record.model_dump()
print(payload["external_firm_id"], payload["domain"], payload["unmapped_fields"])

Feed record.record objects directly into AccountFirmLinker.resolve_batch to get end-to-end CRM automation without requiring customers to hand-map every column.