DEV Community

Michalis Solomou
Michalis Solomou

Posted on

Auto-Creating HubSpot Deals from New SEC Form D Filings with Python

Slack alerts are great for visibility, but they don't do anything by themselves — someone still has to manually create a CRM record before outreach can start. If your team runs on HubSpot, you can skip that step entirely and have a Deal waiting in the right pipeline stage the moment a company files.

This builds on two earlier posts: pulling Form D filings with Python and turning a company name into a website for enrichment. Here we take that enriched data and push it into HubSpot as Deals via the CRM API.

Step 1: Get a HubSpot private app token

In HubSpot, go to Settings → Integrations → Private Apps, create one with crm.objects.deals.write and crm.objects.companies.write scopes, and copy the access token.

Step 2: Pull scored filings from Funding Signals

Same normalized, pre-scored data as the previous tutorials — no need to touch raw SEC EDGAR XML:

import requests
import os

FS_API_KEY = os.environ["FS_API_KEY"]
HUBSPOT_TOKEN = os.environ["HUBSPOT_TOKEN"]

resp = requests.get(
    "https://fundingsignals.net/api/v1/filings",
    params={"days": 1, "min_score": 70},
    headers={"Authorization": f"Bearer {FS_API_KEY}"},
)
filings = resp.json()["results"]
Enter fullscreen mode Exit fullscreen mode

Step 3: Upsert the Company, then create a Deal

HubSpot wants a Company object before it'll let you associate a Deal with it, so we do both in one pass:

HUBSPOT_BASE = "https://api.hubapi.com"
headers = {"Authorization": f"Bearer {HUBSPOT_TOKEN}", "Content-Type": "application/json"}

def upsert_company(filing):
    payload = {
        "properties": {
            "name": filing["company_name"],
            "domain": filing.get("website", ""),
            "industry": filing["industry"],
            "state": filing["state"],
        }
    }
    r = requests.post(f"{HUBSPOT_BASE}/crm/v3/objects/companies", json=payload, headers=headers)
    return r.json()["id"]


def create_deal(filing, company_id):
    payload = {
        "properties": {
            "dealname": f"{filing['company_name']}{filing['industry']} lead",
            "amount": str(filing["amount"]),
            "dealstage": "appointmentscheduled",
            "pipeline": "default",
            "deal_source": "funding_signals",
        },
        "associations": [
            {
                "to": {"id": company_id},
                "types": [{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 5}],
            }
        ],
    }
    requests.post(f"{HUBSPOT_BASE}/crm/v3/objects/deals", json=payload, headers=headers)


for filing in filings:
    cid = upsert_company(filing)
    create_deal(filing, cid)
Enter fullscreen mode Exit fullscreen mode

deal_source is a custom property — create it under Settings → Properties → Deal properties so reps can filter "funding_signals" leads separately from inbound.

Step 4: Add a link back to the original filing

Deals in HubSpot support a description field — drop the source link in there so reps can verify the filing themselves before reaching out:

    payload["properties"]["description"] = (
        f"Source: https://fundingsignals.net/companies/{filing['id']}"
        f"?utm_source=devto&utm_medium=tutorial6"
    )
Enter fullscreen mode Exit fullscreen mode

Why go straight to a Deal instead of a Contact

A Contact with no company context sits in a queue. A Deal already has an amount, an industry, and a pipeline stage attached, so it shows up on a rep's dashboard as something to work today, not something to qualify later. For funding data specifically — where timing is the whole point — skipping the qualification step is the difference between reaching out first and reaching out fifth.

Wire this up against a free Funding Signals API key if you want to see real filings flow into your own HubSpot pipeline instead of stubbing the response.

Top comments (0)