Blending revenue, CRM, and web analytics data in one spreadsheet is the most powerful reporting move most teams never make. Here is the exact structure and methods to do it.
Your Stripe data knows who paid and how much. Your HubSpot data knows how that customer moved through your funnel. Your GA4 data knows how they found you. None of these systems talk to each other by default, and that gap means you are making decisions with at most one-third of the picture.
Combining all three in a single Google Sheet is not complicated in principle. It does require solving three concrete problems: where to join the data, how to handle timing differences, and which tools can actually pull from all three sources. This guide covers all three.
Why Multi-Source Blending Matters
Attribution is the most common reason teams want to blend these three sources. The journey looks like this:
- A visitor finds your product via a paid search ad (GA4 knows this,
sessionCampaignName,sessionMedium). - They fill out a demo form (HubSpot creates a contact and deals record).
- They convert to a paying customer and their first charge processes (Stripe creates a
chargeorsubscriptionobject).
Each step lives in a different system. To answer "which campaigns are generating paying customers, and what is the average revenue per customer from each channel?" you need all three: joined.
Without the join, you are guessing at attribution. With it, you can see that your branded search campaign produces customers with 2.3x the LTV of your Facebook prospecting campaign, and reallocate budget accordingly.
The Three Core Problems to Solve
Problem 1: Data lives in three separate APIs with different schemas. Stripe, HubSpot, and GA4 all have different field names, different data types, and different rate limits. Pulling them all into one place requires either three separate code integrations or a tool designed to abstract that complexity.
Problem 2: The join keys are inconsistent. GA4 tracks anonymous sessions. HubSpot tracks identified contacts. Stripe tracks customers by email or customer ID. Connecting GA4 session data to a specific HubSpot contact requires a user identification event (like a form fill that passes the GA4 client_id to HubSpot), which most teams never instrument.
Problem 3: Different refresh rates create stale joins. If GA4 data refreshes daily, Stripe data refreshes hourly, and HubSpot data refreshes in near real-time, a join at any point in time will include rows where some columns are current and others are hours old. You need to understand what "current" means for each source and design your joins accordingly.
The Tab-Per-Source Pattern
The most maintainable structure for a multi-source workbook is one raw data tab per source, plus one or more dashboard tabs that join and summarize.
Tab structure
- **
ga4_raw**. Daily or hourly GA4 data: date, session source/medium/campaign, sessions, users, conversions, revenue. - **
hubspot_raw**. Contact or deal data: contact email, deal stage, close date, deal value, lead source, campaign UTM. - **
stripe_raw**. Transaction data: customer email, charge date, amount, product, subscription status. - **
dashboard**. Joined view: formulas that VLOOKUP or INDEX-MATCH across the three raw tabs to build a unified picture. - **
config**, A single tab for parameters: date range, property IDs, refresh metadata. Useful for debugging and documentation.
Why this structure works
Raw tabs are append-only or full-replace. The connector or script writes to them directly, and you never edit them manually. Dashboard tabs contain only formulas and formatting, no data you enter by hand. This means refreshes never break your calculations, and you can always trace a number back to its source tab.
Common Join Keys
The join key is the column whose values appear in two or more source tabs and can be used to match rows.
Email address
Most reliable, but not always available in GA4.
Email works well as a join key between HubSpot and Stripe: both platforms store the customer's email. The challenge is that GA4 does not collect email addresses by default (and should not, per privacy best practices). You can pass email as a custom dimension if you fire a GA4 event when a user authenticates, but this requires instrumentation work.
In Sheets: =VLOOKUP(B2, hubspot_raw!A:Z, 5, FALSE) where column B is email in stripe_raw and the formula looks up the matching HubSpot deal stage.
Date
Blunt but often sufficient for campaign-level analysis.
If you aggregate all sources to the day level, date becomes a usable join key. "On March 15, GA4 recorded 430 sessions from paid search, HubSpot logged 12 new deals, and Stripe processed $8,400 in new subscriptions." This is not perfect attribution, but it is directionally useful for spotting correlations and anomalies.
Limitation: Date-level joins conflate users from different channels and cannot tell you whether a specific Stripe customer came from a specific campaign.
UTM parameters passed through to HubSpot and Stripe
Most accurate, requires the most setup.
If you configure HubSpot to capture UTM parameters on form submissions (HubSpot does this automatically for utm_source, utm_medium, utm_campaign when the form is on a page with the HubSpot tracking code), and if you pass those UTM values as metadata on the Stripe customer object when the conversion happens, you can join all three sources on campaign name.
The result is a row in your dashboard tab that says: "The 'April_Webinar_Retargeting' campaign generated X sessions (GA4), Y leads (HubSpot), and $Z in revenue (Stripe)."
The Timing Alignment Problem
Different sources refresh at different rates. This creates a problem when you join them.
- GA4: Processed data has a 24–48 hour delay for some metrics; recent data (sessions, users today) is available within a few hours.
- HubSpot: CRM data is near real-time. A deal closed in HubSpot reflects almost immediately if your connector refreshes frequently.
- Stripe: Payment data is near real-time. Charges appear in the API within seconds of processing.
The consequence
If your dashboard shows a revenue spike yesterday from Stripe, but your GA4 data for yesterday has not yet processed, you cannot immediately correlate it to a traffic source. The join will return blank or stale GA4 values for that date.
How to handle it
- Accept the lag: Design dashboards for data that is at least 48 hours old. This avoids the problem entirely and is appropriate for weekly or trend-based analysis.
- Flag freshness in the sheet: Add a
last_refreshedtimestamp to each raw tab (most connectors write this automatically). Add a formula in the dashboard tab that highlights cells where the GA4 data is more than 24 hours old. - Use separate views for different time horizons: A "Last 90 days" tab can use the joined data with full confidence. A "Last 7 days" tab should show each source independently rather than forcing a potentially misleading join.
Method 1: Manual Export and Paste
Export a CSV from Stripe, a CSV from HubSpot, and a CSV from GA4. Paste each into its raw tab. Build formulas on the dashboard tab.
Why this does not scale:
- Three separate export steps every time you want fresh data.
- Each paste resets the data range, which can break VLOOKUP references if the column order changes between exports.
- No audit trail of when data was last updated.
- The person who does this becomes the report bottleneck.
Manual export is acceptable for a one-time analysis. It is not a reporting workflow for anything that needs to be current more than once a month.
Method 2: IMPORTDATA from APIs
Google Sheets has an IMPORTDATA function that fetches a URL and imports the result. In theory, you could point it at Stripe, HubSpot, and GA4 APIs and have self-refreshing data.
In practice, this approach is fragile for three reasons:
- Authentication: Stripe, HubSpot, and GA4 all require authenticated API calls.
IMPORTDATAcannot send HTTP headers, which means you cannot pass API keys. Workarounds involve building a Google Apps Script web app that acts as a proxy: at which point you have essentially written a full integration. - Rate limits and refresh:
IMPORTDATArefreshes every hour or when the spreadsheet is opened, not on a user-defined schedule. You have no control over when calls go out or how many. - JSON vs. CSV: Most APIs return JSON.
IMPORTDATAworks best with CSV or TSV. You would need an intermediate transformation layer.
IMPORTDATA is a useful quick hack for public CSV endpoints (e.g., a public Google Sheet export URL). It is not suitable for production reporting across authenticated APIs.
Method 3: Multi-Source Connectors
The cleanest solution is a connector that handles authentication and data delivery for all three sources: writing each to its own tab, on its own schedule, without any manual steps.
brooked.io
brooked.io connects to Stripe, HubSpot, and GA4 simultaneously. Each source gets its own configured refresh schedule. You define which data you want from each source (e.g., Stripe charges for the last 90 days, HubSpot deals in "Closed Won" stage, GA4 sessions by campaign), and brooked.io writes each dataset to the tab you designate.
With all three sources feeding into raw tabs, your dashboard formulas do the joining, and the data is always as fresh as your configured refresh intervals.
Coefficient
Coefficient supports HubSpot and Stripe as sources and is well-regarded for CRM reporting. GA4 support is available but more limited than dedicated analytics connectors. If your primary use case is HubSpot + Stripe blending (e.g., lead-to-revenue attribution), Coefficient is a strong option.
Supermetrics
Supermetrics is primarily an analytics connector. It excels at GA4, Google Ads, Meta Ads, and similar marketing data sources. It added HubSpot support, but Stripe is not a native source. For the GA4 + HubSpot combination, Supermetrics works. For Stripe, you would need a second tool.
Example Workbook Layout
Here is a concrete structure you can build in a new Google Sheet.
Tab: ga4_raw
Columns: date | sessionSource | sessionMedium | sessionCampaignName | sessions | newUsers | conversions
Populated by: brooked.io or equivalent, daily refresh.
Tab: hubspot_raw
Columns: contact_email | deal_name | deal_stage | close_date | deal_amount | hs_utm_source | hs_utm_campaign
Populated by: brooked.io or equivalent, hourly refresh.
Tab: stripe_raw
Columns: customer_email | charge_date | amount | product_description | subscription_status
Populated by: brooked.io or equivalent, hourly refresh.
Tab: dashboard
| Date | Campaign | Sessions (GA4) | Leads (HubSpot) | Revenue (Stripe) | Cost per Lead | Revenue per Session |
|---|---|---|---|---|---|---|
| 2025-06-01 | April_Webinar_Retargeting | 430 | 12 | $8,400 | $X | $Y |
The "Leads (HubSpot)" column uses COUNTIFS(hubspot_raw!F:F, A2, hubspot_raw!G:G, B2) to count deals where the UTM source and campaign match the GA4 row.
The "Revenue (Stripe)" column uses SUMPRODUCT((stripe_raw!A:A=date_range)*(stripe_raw!B:B=campaign_condition)*stripe_raw!C:C) or a similar approach: adapted to your actual join key.
Add a last_refreshed row at the top pulling from a metadata tab that logs connector update timestamps.
Connector Comparison Table
| Tool | Stripe | HubSpot | GA4 | Sub-Daily Refresh | No-Code | Best For |
|---|---|---|---|---|---|---|
| brooked.io | Yes | Yes | Yes (15 min) | Yes | Yes | All-three blending |
| Coefficient | Yes | Yes | Limited | Yes | Yes | HubSpot + Stripe focus |
| Supermetrics | No | Yes | Yes (hourly, Pro) | Yes (Pro) | Yes | GA4 + HubSpot |
| Coupler.io | Yes | Yes | Yes | Yes (hourly) | Yes | Mid-size teams |
| Manual export | Yes | Yes | Yes | No | Yes (tedious) | One-time analysis |
| Apps Script | Yes | Yes | Yes | Configurable | No | Dev-led teams |
Troubleshooting
VLOOKUP returns #N/A even though the email appears in both tabs. The most common cause is invisible whitespace. An email pulled from Stripe may have a trailing space that the HubSpot email does not. Use =TRIM() on both sides of the lookup: =VLOOKUP(TRIM(B2), ArrayFormula(TRIM(hubspot_raw!A:A)), 5, FALSE).
GA4 data shows zero sessions for dates where I know there was traffic. Check the date format. GA4 returns dates as integers in YYYYMMDD format. If your dashboard tab uses date serial numbers (how Sheets stores dates internally), the formats will not match. Convert using =DATEVALUE(TEXT(A2,"0000-00-00")) in your GA4 raw tab.
Stripe charges are duplicated in the join. If a customer has multiple charges in the same date range, a VLOOKUP will return only the first match. Use SUMIF or COUNTIFS rather than VLOOKUP for aggregating Stripe revenue. These handle multiple matching rows correctly.
HubSpot UTM fields are empty for most contacts. UTM capture in HubSpot requires the HubSpot tracking script to be active on the page where the form is embedded, and the form must be a HubSpot native form. If you use a third-party form tool (Typeform, Webflow native forms), UTM parameters are not captured by HubSpot automatically. You need to pass them via hidden fields.
Refreshes from different connectors are writing to the same tab simultaneously. Configure each source to write to its own dedicated tab. Never have two connectors writing to the same sheet range: writes can overlap and corrupt data. Use dashboard tabs with formulas to join the data instead.
Bottom Line
Combining Stripe, HubSpot, and GA4 in one sheet is the highest-use reporting project most growth and finance teams are not doing. The data exists. The join logic is manageable. The gap is usually tooling: either three separate exports no one wants to run manually, or a connector that only handles one of the three sources.
The tab-per-source pattern with formula-based joining is the right architecture: raw data stays clean, dashboard logic is transparent, and refreshes do not break your formulas. The remaining question is what populates those raw tabs.
For teams that want all three sources without writing code, brooked.io connects to Stripe, HubSpot, and GA4 in one product with independent refresh schedules per source.
Connect All Three Sources Today
brooked.io pulls from Stripe, HubSpot, and GA4 into separate tabs in your Google Sheet: with schedules as frequent as every 15 minutes. Start your free trial at brooked.io and build your first joined dashboard this afternoon.
Related guides on brooked.io:
- GA4 in Google Sheets: How to Get Hourly Refresh (Not Just Daily)
- Replace Your Weekly NetSuite CSV Export with a Live Google Sheets Connection
- GA4 E-Commerce Data in Google Sheets: Why You Need BigQuery for Product-Level Detail
Troubleshooting quick reference
Frequently asked questions
Do Stripe, HubSpot, and GA4 all need to be on the same plan level for this to work?
No. Each connector connects to each source independently using that source's API. Your GA4 plan, HubSpot tier, and Stripe account type do not need to be at any specific level: only the connector's own plan determines which features are available.
Can I join data at the individual user level, not just the date level?
Yes, but only if you have a consistent identifier across all three systems. Email is the most practical. For GA4, this requires you to send an identify event with the user's email when they log in or submit a form, which stores the email as a GA4 user property. This is additional instrumentation work but enables true user-level attribution.
What is the best join key if I do not have emails in GA4?
Use UTM campaign name as the join key and accept date-level aggregation. This gives you campaign performance across all three sources without requiring user identification in GA4. It answers "which campaigns drive the most revenue?" even if it cannot answer "did this specific customer come from this specific campaign?"
How many rows can Sheets handle for this kind of join?
Google Sheets supports up to 10 million cells. For most reporting use cases (daily GA4 aggregates, monthly Stripe charges, HubSpot deal history), you will not approach this limit. If you are pulling high-volume transaction-level Stripe data (tens of thousands of rows), consider aggregating in the connector before writing to Sheets.
Can I do this without a paid connector?
Yes, using Apps Script. You would write three separate functions (one for each API) and schedule each with a time trigger. The development time is significant (a day or more for someone experienced), and you own all the maintenance. For teams with developer resources, Apps Script is a viable free option.
Ready to connect your data to Google Sheets?
Brooked's free tier covers 100 imports per month with AI Analyst included, no credit card required.
Install Brooked free →

