How to set up Stripe reports

Jun 21, 2024

Creating and analyzing reports for your Stripe data helps you understand how you are making money and how you can improve.

This tutorial shows you how to sync your Stripe data to PostHog and then create a report of insights like customer count, gross revenue, recurring revenue, revenue churn, and more.

Linking Stripe data to PostHog

To start, you need both a Stripe and PostHog account. Once you have those, head to PostHog's Data pipeline page and:

  1. Under the sources tab, click New source
  2. Choose the Stripe option by clicking Link
  3. Enter your account ID and a restricted API key that can read the resources you want to query
  4. Press Next, keep all tables selected and click Import

For Stripe tables, incremental syncs will only sync new records and not update existing records. This is a limitation of the Stripe API in which it's not possible to query for updated data.

Linking Stripe Account

Once done, PostHog will automatically pull and format your Stripe data for querying. You can adjust the sync frequency, see the last successful run, and more in data pipeline sources tab.

Note: If you are missing a table, check your data pipeline sources tab to make sure it synced correctly.

Creating insights for your Stripe report

Now that your Stripe data is synced into PostHog, you can use it to create insights for your report. Each of these requires you to create a new insight in the product analytics tab.

Want to get started fast? Check out our Stripe starter report template.

Customer count

To start, we create a trend of customer count over time.

On the trends tab, change the aggregation to Unique users, and then click the data series, go to the Data Warehouse tab, hover over the stripe_customer table, and click Select. You might want to change the Distinct ID field from id to email as Stripe can give multiple id values to the same user email.

Customer Count Insights

This can also be done for any of the other Stripe data like charges, subscriptions, and invoices. You can also add filters based on property values like created_at, email, status, and more.

Gross revenue

Next, we can get our gross revenue by doing a similar process and selecting stripe_charge. For aggregation, we want Property value sum and then choose amount. We also want to filter out failed charges by clicking the filter button, selecting the status property, and making sure it doesn't equal failed.

Finally, to clean up the visualization, click enable formula mode to divide by 100 (the amount value is in cents) and click Options on the chart to add $ as a prefix.

Gross Revenue Insights

Monthly recurring revenue (average revenue per customer)

There are many ways to calculate monthly recurring revenue, but the easiest and most common is multiplying the number of customers by the average revenue per customer per month.

To do this, we'll rely on the stripe_invoice series. Make sure to set customer_id as the distinct ID field before you select it, and then change the aggregation type to unique users. We also want to filter out invoices with nothing paid, so add a filter where amount_paid is greater than 0.

Next, we can copy that series to create another but modify it to aggregate by property value average of amount_paid.

Finally, we use formula mode to divide the amount by 100 and then multiply by the number of users with (B/100)*A. You can add the prefix and likely want to change the graph to the last 180 days grouped by month.

Monthly Recurring Revenue Insights

Monthly recurring revenue (the Stripe way)

Stripe calculates MRR by "summing the monthly-normalized amounts of all active subscriptions at that time."

To mimic this calculation in PostHog, we need to write an SQL query that gets all the subscription items, normalizes the subscription amount, and then sums them up. Because a lot of this data is in JSON, we need to extract the values.

SQL
WITH subscription_items AS (
SELECT
id,
current_period_start,
JSONExtractArrayRaw(items ?? '[]', 'data') AS data_items
FROM stripe_subscription
WHERE status = 'active'
AND (trial_end IS NULL OR trial_end < now())
),
flattened_items AS (
SELECT
id,
current_period_start,
arrayJoin(data_items) AS item
FROM subscription_items
)
SELECT
sum(
case
when JSONExtractString(JSONExtractRaw(item, 'plan'), 'interval') = 'month'
then JSONExtractFloat(JSONExtractRaw(item, 'plan'), 'amount')
when JSONExtractString(JSONExtractRaw(item, 'plan'), 'interval') = 'year'
then JSONExtractFloat(JSONExtractRaw(item, 'plan'), 'amount') / 12
else 0
end
) / 100 AS current_mrr,
count(DISTINCT id) as subscription_count
FROM flattened_items
WHERE JSONExtractBool(JSONExtractRaw(item, 'plan'), 'active') = true
AND JSONExtractFloat(JSONExtractRaw(item, 'plan'), 'amount') > 0

Why can't we get a rolling MRR? Due to a Stripe API limitation, we only sync new records, not update existing ones. This means when a subscription was created in July but is still active in November, we can't see that it was active for all those months, we only see its current state. For an accurate rolling MRR calculation, we need to know the active state of each subscription for every month in history, but this data isn't available with our current setup.

Revenue churn

For many companies, the amount of money they lose is just as important as the amount they retain. To measure this, we can track revenue churn.

To do this, we write SQL to query both the stripe_invoice and stripe_subscription for users with an invoice but without a subscription. This requires joining the tables together on the customer_id value, and looks like this for the last 30 days:

SQL
with
recent_invoices as (
select customer_id, amount_paid, created_at
from stripe_invoice
where created_at >= now() - INTERVAL 30 day
),
subscribed_customers as (
select DISTINCT customer_id
from stripe_subscription
)
select sum(recent_invoices.amount_paid)/100
from recent_invoices
LEFT JOIN subscribed_customers on subscribed_customers.customer_id = recent_invoices.customer_id
where empty(subscribed_customers.customer_id)

Revenue growth rate

To get revenue growth rate, query stripe_invoice to get the monthly amount paid sum, but then we use window functions to calculate growth. These smooth out the month-over-month changes to give us a 3-month average to use in our growth rate calculation.

SQL
WITH monthly_mrr AS (
SELECT
toStartOfMonth(created_at) AS month,
sum(amount_paid) / 100 AS mrr
FROM prod_stripe_invoice
WHERE status = 'paid'
GROUP BY month
ORDER BY month
),
mrr_with_growth AS (
SELECT
month,
mrr,
avg(mrr) OVER (
ORDER BY month
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS mrr_avg,
avg(mrr) OVER (
ORDER BY month
ROWS BETWEEN 3 PRECEDING AND 1 PRECEDING
) AS previous_mrr_avg
FROM monthly_mrr
)
SELECT
month,
mrr_avg AS mrr,
previous_mrr_avg AS previous_mrr,
(mrr_avg - previous_mrr_avg) * 100.0 / previous_mrr_avg AS mrr_growth_rate
FROM mrr_with_growth
WHERE previous_mrr_avg IS NOT NULL
ORDER BY month

The nice part about this is that we can still visualize the query data as a graph by choosing the line chart option below the query and then choosing month as the X-axis and MRR growth as the Y-axis.

Revenue growth rate graph

Usage by top customers

The great part of syncing your Stripe data in PostHog is using it alongside your product data. An example of doing this is querying the usage of top customers.

To do this, we get the top customers from the stripe_invoice and join their emails with their PostHog distinct_id to get a count of their events.

SQL
with
top_customers as (
select customer_email, sum(amount_paid) / 100 as total_paid
from stripe_invoice
where created_at >= now() - INTERVAL 30 day
group by customer_email
),
big_events as (
select count(*) as event_count, distinct_id
from events
group by distinct_id
)
select customer_email, total_paid, event_count
from top_customers
left join big_events on big_events.distinct_id = top_customers.customer_email
order by total_paid desc

You can further break this down by filtering for specific events like home_api_called.

Top customers usage graph

Further reading

Subscribe to our newsletter

Product for Engineers

Read by 45,000+ founders and builders.

We'll share your email with Substack

Questions? Ask Max AI.

It's easier than reading through 580 docs articles.

Comments

  • Nick
    4 months ago

    How do I access the lazy table / virtual tables in the data warehouse? For example, the persons table has a stripe_customer lazy table, but cannot figure out how to access it. I'm assuming this will help me connect the dots between the two data sources - not sure if it is any smarter than just joining the sources on email address. Thank you!

    • Tom
      4 months agoSolution

      Hi Nick,

      This can be accessed via a SQL insight using HogQL (our SQL query language). See an example below:

      select stripe_customer.id from persons

      or

      select person.stripe_customer.id from events

      Hope this helps!

    • Nick
      Author4 months ago

      Awesome, thanks!

  • Paul
    4 months ago

    When running the revenue churn and usage by top customers queries I get an "all fields must have an alias" error when trying to save them as views.

    image.png

    • Paul
      Author4 months ago

      Screenshot 2024-11-06 at 09.31.52.png

    • 4 months agoSolution

      For a query to be a valid view in PostHog, all fields being accessed must be aliased using the SQL as keyword. The alias names are how you access the fields on the view.(1)

      Here's an example of how to properly alias fields in a SQL query:

      SELECT
      properties.$current_url as current_url,
      count() as current_url_count
      FROM events
      GROUP BY current_url
      ORDER BY current_url_count DESC

      (2)

      To fix your queries, you'll need to add aliases to all selected fields using the as keyword. This includes any fields in aggregate functions or calculations.

  • Paul
    4 months ago

    When running the revenue churn and usage by top customers queries I get an "all fields must have an alias" error when trying to save them as views.

    image.png

    • Ian
      4 months agoSolution

      Can you try adding an alias using as to all the columns in your SELECT command?

    • Paul
      Author4 months ago

      Yeah, that did the trick!

  • Nick
    5 months ago

    It seems the MRR query may only work for the current month as it relies on the following where clause, which I think is just a point in time representation:

    JSONExtractBool(JSONExtractRaw(item, 'plan'), 'active') = true

    Any ideas on how to handle? The MRR is quite closely, though not exactly, tied to the Stripe dashboard if I just look at the current month but previous months are way too low.

    • 4 months ago
      This answer was marked as unhelpful.

      You're correct about the MRR discrepancy between the Stripe dashboard and the query results(1). This appears to be a known issue where the query may only reflect current active subscriptions rather than historical data(1).

      For measuring revenue at PostHog, we typically look at two key metrics - MRR (monthly recurring revenue) and NRR (net revenue retention)(2)(3). These metrics can be accessed through revenue dashboards(2).

      I wasn't able to find a direct answer about modifying the specific query to handle historical MRR calculations. You can get more help at Support or GitHub.

      Check out these potentially useful sources: How to set up Stripe reports SQL (beta) Sales operations

  • Fraser
    5 months ago

    The stripe dashboards don't seem to handle currency well. Its just adding up the value regardless of currency, so if you have some transactions in a high denomination currency like Indian Rupee it completely skews the data. Is there a way to either convert all to USD or to only look at USD transactions perhaps?

    • Ian
      5 months ago

      Hi Fraser, you could filter the currency property to only usd.

PostHog.com doesn't use third party cookies - only a single in-house cookie.

No data is sent to a third party.

Ursula von der Leyen, President of the European Commission