Knowing Which PostHog NPS Detractors Are Your Highest-Value Accounts
You cross-reference two lists by hand: PostHog's detractor scores and your Stripe subscription data, matched on whatever identifier both happen to carry, usually an email or a workspace ID. PostHog doesn't do this join for you. It scores NPS correctly and it will show you exactly who rated you 0 through 6, but a detractor score has no revenue attached to it anywhere in the product, because NPS lives in product analytics and ARR lives in your billing system.
That gap isn't a missing checkbox you overlooked. PostHog's own survey results page filters by event properties, person properties, feature flags, cohorts, and HogQL expressions, none of which includes revenue unless you've separately synced it in and written the query yourself. The rest of this guide is that query, the two-minute version and the honest limit of it, plus the worked example of what happens when nobody runs it for a month.
What PostHog actually gives you out of the box
Start with what's real, because most of the setup is already done for you. PostHog's NPS survey template computes the standard score correctly: promoters rated you 9 or 10, detractors rated you 6 or below, and the tutorial has you subtract the detractor count from the promoter count to get the score. That part isn't the gap.
The gap is in viewing results. You can filter and break down survey responses by event properties, person properties, feature flags, cohorts, and HogQL expressions, and PostHog even exposes a getSurveyResponse() HogQL function to pull a specific answer out of the response event for exactly this kind of custom query. What's not on that list is a native promoter/passive/detractor category filter, or anything that reaches into a billing table. Segmenting by NPS bucket at all is an open feature request, still unresolved as of this writing, asking PostHog to "borrow some of the filtering logic we use when setting up conditional questions" so a detractor cohort is one click instead of a manual score range. Today, you'd build that range filter yourself with a person or cohort property comparison.
Revenue is a separate problem again. If you've connected Stripe as a data warehouse source, PostHog syncs your charges, customers, invoices, products, subscriptions, and dozens of other Stripe objects into queryable tables. What the docs don't hand you is a join: nothing in PostHog's Stripe source documentation defines a ready-made link between a synced Stripe table and a person or group record. You write the HogQL that matches a Stripe customer's email or ID to a PostHog person, same as you'd write any other query across two tables that happen to share a key.
The two-minute version, and where it breaks
The workable version of this join, run by hand, looks like this:
- Pull detractors. In PostHog, filter NPS survey responses to
$survey_responsescores of 6 or below (or wire up a HogQL expression if you want the exact promoter/passive/detractor buckets from the open request above). - Pull the billing key. Export or query the matching person property, typically an email or workspace ID, for each detractor.
- Cross-reference in Stripe. Search each key in the Stripe Dashboard or your synced warehouse table, and note the account's current subscription value.
- Sort by ARR. The detractors worth an immediate call are the ones at the top, not the ones who happened to leave the angriest comment.
Nothing about this is technically hard. HogQL can do steps 1 and 2 in one query if the properties are named consistently, and a warehouse view can join Stripe's customer table to PostHog's persons table on email if both are clean. The honest limit is that "if both are clean" is doing a lot of work in that sentence, and the query has to be rebuilt, rerun, or at minimum re-trusted every time someone asks for the current list, because neither system tells the other one anything changed.
Shiplog's fourteen detractors, one of them worth $61,000
The quarter Shiplog, a deployment-pipeline tool for engineering teams billed per workspace through Stripe, crossed $2M in ARR, its quarterly NPS report came back with fourteen detractors on it. Mira Sandoval owns retention there, and the list PostHog handed her had no ARR column, so she'd always worked it in the order the responses landed, oldest first.
Three calls into that order, an account manager mentioned in passing that one of the workspaces further down the list, a name that gave no hint of its size, was Shiplog's single largest customer at $61,000 a year. It had rated the survey a 2.
Mira said as much afterward, in a message to the rest of the retention team: "I've been on the phone with $8k accounts for two days and our biggest detractor by ARR was sitting at position eleven on my list. I didn't know it was worth calling first."
She pulled the Stripe subscription value for all fourteen names after that, and the ranking flipped. Four of the fourteen accounted for more than 70% of the ARR at risk in that list; the other ten were smaller workspaces whose churn would sting less than losing the $61,000 account over a support gap nobody had prioritized calling about. Submission order had put a $4,200 account ahead of it for no reason connected to either account's value or urgency, just the timestamp on when each person happened to fill out the survey.
Shiplog's fix wasn't a new tool. It was a standing HogQL query against the Stripe-synced warehouse table, joined to detractor responses on workspace ID, rerun before every quarterly call list went out. It worked, and it also meant someone had to remember to rerun it, keep the workspace ID property clean on both sides, and manually rebuild the join whenever a workspace changed its billing email.
Where the manual join stops being enough
The two-minute version above holds up fine at low volume, for a handful of detractors a quarter, checked by one person who remembers to run the query. It stops holding up on a few predictable edges:
- The identifiers drift. A workspace's billing email in Stripe and its identified person property in PostHog only match as long as nobody changes one without the other, and nothing forces them to stay in sync.
- The query is a standing liability, not a one-time build. Someone has to remember it exists, rerun it before every review, and notice when a schema or property name changes underneath it.
- NPS and revenue are half the picture. A detractor's complaint usually also shows up in a support ticket or a Slack thread, and none of that context rides along with the ARR number unless someone copies it in by hand too.
That's the point where we'd mention Modem. Its PostHog integration and Stripe integration both resolve to the same underlying company record, so a detractor's account and its subscription value are already the same row, not two lookups joined on a hope that the identifiers matched. When a low score comes in, the agent already knows what that account pays and can route the follow-up as a Linear issue carrying both the NPS comment and the ARR figure, instead of a name and a number that need a person to connect them. Modem is our product, so read that description with the source in mind. Set against building this yourself in PostHog, the honest comparison is narrow: Modem doesn't touch your NPS scoring or your Stripe billing at all, it only removes the standing query that ties them together.
Two guides worth reading alongside this one: seeing who actually submitted a PostHog survey response covers the identity problem that has to be solved before any of this join works, and why PostHog Revenue Analytics says a canceled Stripe customer is still active covers a separate, sharper way the Stripe side of this can quietly go stale.
Start with the accounts, not the scores
If you're running this by hand today, don't start with the lowest score. Pull your detractor list, pull each account's current subscription value from Stripe, and sort by ARR before you make a single call. The angriest comment and the most expensive account are frequently two different rows, and the list order PostHog gives you by default has no idea which one it's showing you first.
