Platform guide

Bringing external company signals into HubSpot

Fokals is delivered direct, and your pipeline writes it to HubSpot through the CRM API. Here is the property design, the domain match, the batch update and the traps, with SQL and the HubSpot calls.

Updated 5 October 20267 min read

This guide shows how to put a small set of dated company signals from a licensed feed onto HubSpot company records: which properties to create, how to match your companies to the feed, how to write the values in batches through the CRM API, and what goes wrong. It assumes you have loaded Fokals datasets into a database you control. Fokals is delivered direct, by REST API and as bulk files, and your team writes the values to HubSpot through the CRM API with the steps below. Facts about HubSpot come from its documentation, read on 4 October 2026 and linked below.

Decide what belongs on a company record

A HubSpot property holds one value per record, so it suits a current state and not a history. Put the state your team acts on in HubSpot and keep the full datasets in your warehouse. Seven properties are enough to start, drawn from the intent, hiring and marketing stack datasets. Give each the prefix fokals_ so that its origin is plain and a list or workflow filter can find them all.

PropertyType and field typeDataset and fieldWhat it holds
fokals_company_idstring, text, unique valueCompany ID (company_id)The stable key that every later update uses
fokals_signals_as_ofdate, dateHiring Activity, company_hiring_daily.dayThe latest closed day the run read
fokals_intent_top_topicstring, textIntent Scores, company_intent_weekly.topic_labelThe topic with the highest score in the latest closed week
fokals_intent_top_scorenumber, numberIntent Scores, company_intent_weekly.scoreThat score from 0 to 100, or 0 when no topic scored 5 or more
fokals_intent_surgebool, booleancheckboxIntent Scores, company_intent_weekly.surgeTrue when any topic surged that week
fokals_new_postings_28dnumber, numberHiring Activity, company_hiring_daily.new_postingsNew postings over the last 28 closed days
fokals_last_tech_changedate, dateTechnology Changes, company_tech_events.observed_atThe day of the latest technology added or removed

Two choices need a reason. fokals_signals_as_of carries the date the values describe, because HubSpot's own timestamps show when a property was written and not when the signal was observed. And fokals_intent_top_score is 0 when a company has no row for the week: Intent Scores carries a row for each topic scoring 5 or more, so a missing row means every topic scored below 5, and the property description should say so. HubSpot's company read endpoints accept a propertiesWithHistory parameter, so each weekly write also leaves earlier values to read back inside HubSpot.

Create the properties through the API

Create the properties by API, not by hand, so that the definitions live in version control. HubSpot's properties guide lists the types and the field type each allows, and its create property reference lists the fields of a request: name, label, type, fieldType, groupName, description and hasUniqueValue. Create a property group in HubSpot's property settings first, and pass the internal name of that group as groupName. The call needs the crm.schemas.companies.write scope. The key property comes first, created as a unique identifier.

curl --request POST \
  --url https://api.hubapi.com/crm/properties/2026-09/companies \
  --header "Authorization: Bearer $HUBSPOT_SERVICE_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "fokals_company_id",
    "label": "Fokals company ID",
    "groupName": "fokals_signals",
    "type": "string",
    "fieldType": "text",
    "hasUniqueValue": true,
    "description": "company_id from the Fokals company index. Written by a script."
  }'

HubSpot allows up to ten unique ID properties per object, so the key uses one of them. A date property takes an ISO 8601 date such as 2026-10-03, and the other six properties go in one request to the batch create endpoint.

The paths carry the API version. These examples use 2026-09, the latest in HubSpot's reference on the day of checking, and HubSpot says an earlier date version keeps working until its end-of-life date. Hold the version in one constant and review it when the next is published.

For the credential, HubSpot's documentation presents service keys as the account-level credential for system-to-system integrations, with scopes chosen per key. A changelog entry says creating new legacy private apps is disabled from 28 September 2026 for accounts created on or after that date and from 26 October 2026 for older accounts, while existing private apps keep working. Give the key only the scopes that HubSpot's reference lists for the calls you make: crm.schemas.companies.write to create properties, and the company read and write scopes for the batch calls. Confirm on the service keys page that your key type accepts them.

Match your companies to the feed

Fokals identifies a company by its company ID. HubSpot identifies it by record ID and, to avoid duplicates, by domain: its company guide calls the domain the primary unique identifier, and says a company with several domains can list the others in hs_additional_domains, separated by semicolons. So match on domain once, then store the company ID on the HubSpot record and use it for every later update. A changed domain in HubSpot then no longer breaks the link. This is domain matching, in four steps.

  1. Build the Fokals side from Technology Stack (company_technologies), which carries domain beside company_id. A company can have more than one website, so keep one row per company and domain.
  2. Page through HubSpot's list endpoint for companies, asking for domain and hs_additional_domains with the properties parameter, and load the result. Do not search one domain at a time: HubSpot documents stricter limits for its search endpoint.
  3. Normalise both sides (lower case, no scheme, no www.), turn the additional domains into rows of their own, and join.
  4. Write the matches that are one to one, and send the rest for review.
create table fokals_sites as
select distinct company_id, lower(regexp_replace(domain, '^www\.', '')) as domain
from company_technologies;

-- hubspot_companies: hs_object_id and one normalised domain per row
select
  h.hs_object_id,
  f.company_id,
  count(*) over (partition by h.hs_object_id) as companies_per_record,
  count(*) over (partition by f.company_id)   as records_per_company
from hubspot_companies h
join fokals_sites f on f.domain = h.domain;

Write fokals_company_id to each one-to-one match with a batch update to https://api.hubapi.com/crm/objects/2026-09/companies/batch/update. For this first write the id is the HubSpot record ID and there is no idProperty.

{
  "inputs": [
    { "id": "<hs_object_id>", "properties": { "fokals_company_id": "<company_id>" } }
  ]
}

A unique property cannot hold one value on two records, so two HubSpot companies that share a domain cannot both take the Fokals ID. The response lists the failure, and you deduplicate in HubSpot.

Measure the match before you rely on it. The match rate is matched HubSpot companies divided by HubSpot companies that have a domain, read by segment. Unmatched companies fall into three groups: companies outside the Fokals index of listed companies, the brands they own and verified private companies; websites on which no technology is recorded yet, which have no row in Technology Stack; and records with no usable domain. A brand or subsidiary carries the listing identifiers of its listed parent, so an ISIN or FIGI alone cannot tell a brand from its parent. Match on the company ID, and use the listing identifiers only to roll up.

Write the signals in batches

Compute one row per matched company. The query below gives the values in the table above. Its date arithmetic and boolean aggregate differ between warehouses, so adapt them to yours. matched is the list of company IDs you wrote in the last step.

with wk as (select max(week_start) as week_start from company_intent_weekly),
ranked as (
  select company_id, topic_label, score,
         row_number() over (partition by company_id order by score desc, topic) as rn
  from company_intent_weekly
  where week_start = (select week_start from wk)
),
surges as (
  select company_id, bool_or(surge) as any_surge
  from company_intent_weekly
  where week_start = (select week_start from wk)
  group by company_id
),
hiring as (
  select company_id, sum(new_postings) as new_28d
  from company_hiring_daily
  where day > (select max(day) from company_hiring_daily) - 28
  group by company_id
),
tech as (
  select company_id, cast(max(observed_at) as date) as last_change
  from company_tech_events
  where category = 'technology' and change in ('added', 'removed')
  group by company_id
)
select
  m.company_id,
  (select max(day) from company_hiring_daily) as as_of,
  coalesce(r.topic_label, '')  as top_topic,
  coalesce(r.score, 0)         as top_score,
  coalesce(s.any_surge, false) as surge,
  coalesce(h.new_28d, 0)       as new_28d,
  t.last_change
from matched m
left join ranked r on r.company_id = m.company_id and r.rn = 1
left join surges s on s.company_id = m.company_id
left join hiring h on h.company_id = m.company_id
left join tech t on t.company_id = m.company_id;

Then send the rows. HubSpot's object API guide limits batch endpoints to 100 inputs per request, and the batch update takes the unique property as idProperty with the Fokals ID as id. A partly failed batch returns status 207 with an errors list, and a passed limit returns 429. HubSpot's usage guidelines give burst and daily limits by subscription tier and note that the search endpoint has limits of its own.

import os, time, requests

URL = "https://api.hubapi.com/crm/objects/2026-09/companies/batch/update"
HEADERS = {"Authorization": f"Bearer {os.environ['HUBSPOT_SERVICE_KEY']}"}

def post_batch(inputs):
    for attempt in range(5):
        resp = requests.post(URL, headers=HEADERS, json={"inputs": inputs}, timeout=60)
        if resp.status_code != 429:
            break
        time.sleep(2 ** attempt)          # back off, then retry the same batch
    resp.raise_for_status()               # 200, or 207 when some records failed
    return resp.json().get("errors", [])

def push(rows):
    for i in range(0, len(rows), 100):    # 100 inputs per request
        inputs = []
        for r in rows[i : i + 100]:
            props = {
                "fokals_signals_as_of": str(r["as_of"]),
                "fokals_intent_top_topic": r["top_topic"],
                "fokals_intent_top_score": str(r["top_score"]),
                "fokals_intent_surge": str(r["surge"]).lower(),
                "fokals_new_postings_28d": str(r["new_28d"]),
            }
            if r["last_change"]:
                props["fokals_last_tech_change"] = str(r["last_change"])
            inputs.append({"id": r["company_id"], "idProperty": "fokals_company_id",
                           "properties": props})
        for error in post_batch(inputs):
            print("failed:", error)

Send only what changed. Keep the last values you pushed in a table and write only the companies whose row differs. Intent Scores is written weekly, Hiring Activity daily and Technology Changes daily to weekly, so most daily runs touch few records. Run the job after your daily load, and read the guide to incremental sync for the bookmark that keeps the load itself small.

Keep your properties apart from HubSpot's enrichment

HubSpot's help article says automatic enrichment does not overwrite values set by a user or another system, and that continuous enrichment stops for a property once a user or another system edits it. The guide to HubSpot's data enrichment describes the modes. So never write Fokals values into a property that HubSpot enriches, such as industry or number of employees. Use the fokals_ properties and compare the two sources in a report. Say in each property description that a script writes the value, because a hand edit is overwritten on the next run.

Other routes into HubSpot

For a one-off or small load, HubSpot's import tool can update existing companies by matching on record ID, company domain name or a custom property that requires unique values. For a standing feed from a warehouse, HubSpot's page on cloud data storage integrations says warehouse integrations move data into and out of HubSpot on schedules from hourly to daily and are available with Data Hub Enterprise. Both leave the design above unchanged: the properties, the key and the as-of date.

Where this stops

A property holds a current value, so the point-in-time history of a signal stays in the Fokals datasets and not in HubSpot. Fokals data is company-level, so every property in this guide sits on a company record. Whether a use is internal, embedded in a product or redistributed is set by the written licence, so settle it before the properties are visible to anyone outside your organisation. The data dictionary defines every column used.

Frequently asked questions

Can I update HubSpot companies by domain without a custom ID property?

HubSpot's documentation lists the company domain as a unique identifier, and its import tool can match on it, but its upsert guidance names custom unique identifier properties and contact email only. A custom property that holds the Fokals company ID avoids depending on a domain, which can change or be shared, and keeps the link when a company changes its domain.

How many companies can I update in one request?

HubSpot's object API guide limits batch endpoints to 100 inputs per request, so 10,000 matched companies need 100 requests. Burst and daily limits depend on your subscription tier, and HubSpot answers a passed limit with a 429 status. Sending only the companies whose values changed keeps a daily run well inside the limits.

How does Fokals data get into HubSpot?

Fokals is delivered direct, by REST API and as bulk files. A script or pipeline that you run reads the Fokals data from your warehouse and writes the values to company properties through the HubSpot CRM API, matching on the domain once and on the stored company ID afterwards, as this guide shows.

Which credential should the script use?

HubSpot's documentation presents service keys as the account-level credential for system-to-system integrations, with scopes set per key. Legacy private apps keep working where they exist, but HubSpot's changelog says new ones can no longer be created on new accounts from 28 September 2026 or on existing accounts from 26 October 2026.

How do I see how a signal changed inside HubSpot?

Read the company with the propertiesWithHistory parameter, which HubSpot documents for its company read endpoints, to get earlier values of the Fokals properties as well as the current ones. The point-in-time history of the underlying signals stays in the Fokals datasets, where each record carries the time it was observed.

The queries and code on this page are examples to adapt. Test them in your own environment before you rely on them.

What this page says about the products it names was checked against their public documentation on 4 October 2026. Product and company names are trademarks of their owners. Fokals is not affiliated with them or endorsed by them.