Sync PostHog Groups to Your CRM

Sync PostHog Groups to Your CRM

Sync PostHog Groups to Your CRM

Photo of Utku Zihnioglu

Utku Zihnioglu

CEO & Co-founder

Your sales team opens HubSpot to look at Acme Corp before a renewal call. They see two contacts, three closed deals, and zero idea that Acme has logged in twice this month and that one of those logins was a single trial user poking around an empty workspace. The actual product usage is sitting in PostHog, organized under PostHog groups. It is attached to a group called acme. HubSpot has never heard of it.

This is the standard B2B PostHog setup. Person-level events flow into PostHog cleanly, group analytics work great inside the PostHog dashboard, and then the data stops at PostHog's edges. Getting that group data into the rest of your stack is where most teams get stuck. The CRM your sales team actually uses doesn't see them. The warehouse your analysts query doesn't either. Neither does the marketing tool that sends renewal emails.

This post walks through what these groups actually are, why standard CRM sync ignores them, and the practical mechanics of getting that data out.

What PostHog groups are and why B2B teams use them

A group record in PostHog represents something other than a person. In B2B PostHog deployments that "something" is almost always a company, a workspace, or a team. You define the group type once, give every relevant event a $groups property pointing at that group, and PostHog starts aggregating usage at the company level instead of the user level.

The mechanics involve three pieces:

  • Group type: the kind of entity you're modeling. Common ones are company, team, organization, or account. PostHog supports up to five.

  • Group key: the unique identifier for a single instance of that type. For a company group, this is usually the company's domain, an internal account ID, or a Stripe customer ID.

  • Group properties: the free-form key/value bag attached to that group. Plan tier, MRR, signup date, vertical, employee count, anything you want to slice on.

You set them in your application code with posthog.group() (or the equivalent server-side call) and PostHog stitches every subsequent event from that user to the group. That is what powers DAU by company, retention by plan tier, and funnels filtered to a specific account.

The result is the only thing in most B2B stacks that genuinely captures product usage at the account level. CRMs track deals and conversations. Billing tracks invoices. PostHog tracks what the customer actually did. That third dataset is the most underused asset in B2B PostHog setups, and the reason is almost always that it never leaves PostHog.

Why your CRM doesn't see PostHog groups

CRMs are built around two record types: people (contacts/leads) and companies (accounts). Most CDPs and reverse-ETL tools mirror that assumption. They sync Person from PostHog to Contact in HubSpot. They sync User from your database to Lead in Salesforce. They do not, by default, know that this third record type exists, much less how to map it to a CRM company.

A few specific things break:

  • The PostHog API splits groups by group_type_index, not by a clean /companies endpoint. If a sync tool wants to pull "companies," it has to know which numeric index your company group type was registered under. Index drift across environments is a real problem.

  • Group keys are strings, not IDs. They might be domains (acme.com), they might be UUIDs, they might be your internal org_<nanoid> identifiers. The CRM, on the receiving end, has its own primary key. You need a deterministic way to match the two.

  • Group properties are sparse. PostHog only stores a property after $groupidentify has set it at least once. Properties you haven't backfilled simply don't exist on older groups.

  • Most reverse-ETL tools are built warehouse-first. They assume you can already query posthog.groups from Snowflake or BigQuery, which means you've set up Snowpipe or PostHog's S3 export and built a job to land it. That is a lot of infrastructure for what is structurally a CRM-to-CRM sync between PostHog and HubSpot.

PostHog company sync is, on paper, a simple record sync. In practice it falls into the gap between PostHog's group model and the rest of the world's "company object" model. You either build the bridge yourself or pick a tool that's done it for you.

How to sync PostHog groups to your CRM, warehouse, or marketing tool

There are three architectural shapes for getting PostHog groups out of PostHog. Pick based on how much pipeline you already run.

Approach

When it fits

What you give up

Warehouse-mediated

You already pipe PostHog events into Snowflake, BigQuery, or a Postgres replica

Latency (typically 1-24 hours) and a SQL job to flatten group properties

Direct sync

You want PostHog groups in a CRM or marketing tool with minimal infrastructure

Some flexibility for joining with non-PostHog tables before sync

Custom script

One-off backfill or unusual destination

Maintenance, retries, schema drift handling, observability

The warehouse path is the path most data teams default to. Pull PostHog's groups table into the warehouse via PostHog's data export, write a dbt model that flattens group_properties_<index> into named columns, and then point a reverse-ETL tool at that view to push into HubSpot or Salesforce. It works, but it adds a warehouse round-trip and a daily refresh window that you mostly don't need for a simple "keep CRM company records current" use case.

Direct sync skips the warehouse. The sync tool authenticates to PostHog with a personal API key, calls the groups list endpoint filtered by group_type_index, and treats the result as a record stream. Group properties become source columns. The destination receives those columns mapped to fields, whether that destination is HubSpot Company, Salesforce Account, or Customer.io Object. No warehouse, no dbt, no orchestration. The trade-off is that anything you want to join in (billing, support data) has to be reachable from the same sync tool.

The custom-script approach is fine for backfills and proofs of concept. It is a bad place to land long-term, because PostHog API rate limits, schema drift, and CRM upsert quirks all need to be handled, and your weekend Python project will eventually break on a Sunday.

If you want to read the canonical PostHog explanation of the group model and the underlying API, PostHog's group analytics documentation walks through the calls and event structure in detail.

A concrete recipe: PostHog → HubSpot Companies

The simplest working setup looks like this:

  1. In PostHog, confirm the group_type_index of your company group. Settings → Data Management → Groups shows the index next to each group type.

  2. Pick a group key strategy. Domain (acme.com) is the most portable because HubSpot's company object also keys naturally off domain.

  3. Decide which group properties matter. Plan tier, signup date, last login, MRR, seat count are the usual five. Skip anything that's not also useful in CRM context. Debug fields, internal flags, and computed metrics that change every minute belong in PostHog, not in your CRM.

  4. In your sync tool, define a sync from PostHog group type N to HubSpot Company. Map group_keydomain, then map each chosen property to its HubSpot field. Create the HubSpot custom properties first if they don't exist; HubSpot's company properties API covers the call.

  5. Run incrementally. Pulling all groups every five minutes is fine for most setups; PostHog's groups endpoint is paginated and not punitively rate-limited.

The same shape works for Salesforce Accounts, ActiveCampaign Accounts, Customer.io Objects, or any destination with a company-shaped record. The mapping changes; the architecture doesn't.

Mapping PostHog group properties to CRM company fields

Once the connection works, the interesting question is what to actually map. Group properties in PostHog are loose. They accumulate over time. Most of what's there is product-team-shaped, and most of what your sales team needs is sales-team-shaped. The two overlap less than people expect.

A few mapping rules that hold up across deployments:

  • Always map the group key to the destination's primary identifier. This is what makes the sync stable across runs. If you key on something else, you'll get duplicates the first time someone changes a name.

  • Don't sync every property. PostHog groups can carry hundreds of properties once you've been running for a while. Pick the ten that drive a sales or support decision and ignore the rest. Mapping the full firehose creates noise in the CRM and slows every sync.

  • Type-match before the first run. PostHog stores everything as JSON. Numeric MRR comes through as a string in some SDKs. Casting once at the sync layer beats discovering type mismatches in HubSpot's "Sorry, this property only accepts numbers" error.

  • Reserve one property for "last seen in PostHog." It's the single most useful column for sales: when did this account actually use the product. PostHog tracks it natively per group; surface it in the CRM as last_product_activity_at or similar.

PostHog group properties also evolve. New ones appear when your app starts setting them via $groupidentify. A sync tool that re-discovers the property list at sync time, rather than freezing the schema at config time, handles this gracefully. A tool that requires you to re-define the schema every time a property is added does not.

Common PostHog groups pitfalls: group_type_index, missing properties, and stale data

Three things go wrong often enough to call out specifically.

Wrong group_type_index. The most common failure mode. You set up the sync against index 0, but index 0 in your dev project is team and index 0 in production is company. The sync runs, returns rows, and silently writes the wrong data. Always verify the index in the PostHog UI for the project you're syncing from. If you have multiple projects, document the indexes. PostHog itself does not enforce that index N means the same thing across projects.

Missing properties. A group only has the properties that have been set on it. If plan_tier only started getting written six months ago, every group created before that date will have it as null. This looks like a sync bug ("why are half my companies missing plan?") but it is a backfill problem. Either run a one-time $groupidentify against historical groups or accept that the property is forward-looking only.

Stale data from batch warehouse loads. Teams running the warehouse-mediated path often see CRM data lagging real product usage by a full day. The cause is usually the warehouse load schedule, not the sync. PostHog updates the source-of-truth group record as events come in, but if your warehouse only refreshes once daily, the CRM sync is downstream of that 24-hour gate. The fix is either a tighter warehouse refresh or, for operational sync use cases, skipping the warehouse entirely.

Identifier collisions across environments. Group keys are often not globally unique. Both staging and production use acme.com for the same company, for instance. A misconfigured sync source can clobber CRM data with test values. Always isolate dev and prod PostHog projects, and don't reuse keys across environments unless you've explicitly thought through what happens when a single CRM record receives data from both.

Where Oneprofile fits

The reason this post exists is that we shipped first-class group sync support in our PostHog SDK. Connect your PostHog project, pick a group_type_index, and company groups appear as a record type alongside Person. From there you can map them to HubSpot Companies, Salesforce Accounts, a Postgres table, or any other destination Oneprofile speaks. Group property listing happens at sync time so new fields show up without reconfiguring the connector. No warehouse required, no SDK code to write, no dbt model to maintain.

If you have a warehouse and want to keep using it, Oneprofile reads from it just as happily. The point is that you should not be forced into the warehouse path just because the connector ecosystem assumes you have one.

Account-level group data is one of those assets that's underused mostly because it's hard to get out of PostHog. Getting it out is a sync problem, not an analytics problem. Treat it that way and the rest follows.

What are PostHog groups?

How do I export PostHog groups to HubSpot?

What is group_type_index in PostHog?

Can I sync PostHog groups without a data warehouse?

Why are some PostHog group properties missing in my CRM?

Ready to get started?

No credit card required

Free 100k syncs every month