SteerAds
TutorialStripeConversion importOffline conversions

Stripe Revenue → Google Ads Conversion Import (Tutorial) 2026

Complete 2026 tutorial for importing Stripe revenue into Google Ads as offline conversions — GCLID capture in Stripe metadata, conversion action setup, currency handling, refund adjustments, BigQuery middleware, and a 30-day rollout.

Matt
MattTracking & Data Lead
··8 min read

For most SaaS founders and growth teams running Google Ads in 2026, the gap between the ROAS Google Ads reports and the revenue Stripe actually collected is somewhere between 20% and 70%. Web pixel conversions fire on checkout intent (button click, page load); they don't know about failed cards, dunning bounces, refunds, fraudulent disputes, or trials that never converted. Smart Bidding optimizes against the signal you give it — and if that signal is gross checkout intent rather than net collected revenue, the algorithm scales spend in the wrong direction.

This guide walks through the complete technical and operational integration: capturing the Google Click ID (GCLID) on every checkout page, storing it in Stripe customer or charge metadata, subscribing to the right webhook events, calling the Google Ads ConversionUploadService API with the correct conversion action and value, handling multi-currency Stripe accounts, and wiring refunds and disputes through the conversion adjustment API. We close with a BigQuery middleware pattern for high-volume accounts and a 30-day implementation rollout.

Why web pixel conversions overstate ROAS — and Smart Bidding loves the inflation :

A typical SaaS web pixel fires the conversion event when the customer clicks "Subscribe" or lands on the thank-you page. In Stripe terms, that's the equivalent of payment_intent.created or checkout.session.completed — not charge.succeeded. The pixel doesn't know about: 3DS authentication failures, declined cards, fraud holds, free trials that never convert, downgrades during the first billing cycle, full refunds in the first 30 days, partial refunds and credits, or chargebacks. Across the Stripe-using SaaS accounts we've audited in 2024-2026, web pixel conversions over-report collected revenue by 15-35% in self-serve products and 30-60% in trial-led products. Smart Bidding doesn't know it's chasing inflated numbers — it scales spend on the loudest signal, even if that signal is wrong by 40%.

Why Stripe → Google Ads conversion import matters in 2026

Three trends in 2026 make this integration more important than it was in 2022:

1. Smart Bidding now consumes essentially all account spend. Per Google's 2025 transparency reporting, more than 85% of Google Ads spend across active accounts in 2026 flows through bid strategies driven by conversion signal — Maximize Conversions, Maximize Conversion Value, Target CPA, and Target ROAS. The bid strategy is only as accurate as the conversion data it receives. Manual CPC accounts can tolerate noisy conversion signal because humans review every bid; Smart Bidding cannot. If your conversion data over-reports by 30%, Smart Bidding spends 30% more than it should on bids that look profitable but aren't.

2. The 2024 third-party cookie deprecation accelerated GCLID importance. As browser-side tracking degrades, server-side conversion import via GCLID becomes the most reliable bridge between an ad click and a downstream conversion. Stripe is the natural source of truth for revenue events because it sits at the financial settlement layer, post-payment-method-validation. Pixel-based attribution will keep weakening through 2026-2027; server-side Stripe-based attribution stays accurate.

3. ROAS-based bidding requires honest revenue input. Google Ads' Target ROAS bid strategy works by predicting conversion value per click and bidding to a target value-to-cost ratio. If the values it's predicting include refunded revenue, failed-trial revenue, and disputed charges, the predictions are systematically high — and the bidding overshoots. Stripe import is the only mechanism that gives Google Ads ground-truth net revenue at the conversion-value granularity Target ROAS needs.

The integration isn't optional infrastructure for serious SaaS in 2026; it's the floor below which Smart Bidding doesn't work properly.

The data flow at a glance: GCLID → Stripe → Google Ads

The end-to-end data flow is straightforward in concept and full of edge cases in execution. The five steps:

Step 1 — Click: A user clicks your Google Ad. Google Ads autotagging appends ?gclid=ABC123... to the destination URL. The GCLID is a unique token tied to that click, valid for 90 days for conversion attribution on Search/Display campaigns.

Step 2 — Capture: Your landing page reads the gclid query parameter and persists it. Two persistence patterns: cookie (first_touch_gclid for 90 days), or backend storage keyed by anonymous session ID then user ID after signup.

Step 3 — Checkout: When the user reaches Stripe Checkout (or the Stripe Elements form on your own page), pass the stored GCLID as a metadata field on the Checkout Session creation, the PaymentIntent, or the Customer object. This binds the GCLID to the financial event.

Step 4 — Charge succeeds: Stripe processes the payment. On successful settlement (charge.succeeded), Stripe fires a webhook to your endpoint. The webhook payload contains the charge ID, amount, currency, customer ID, and metadata (including the GCLID you stored).

Step 5 — Upload to Google Ads: Your webhook handler calls the Google Ads API ConversionUploadService.UploadClickConversions endpoint, passing GCLID, conversion action resource name, conversion timestamp, conversion value, and currency code. Google Ads matches the GCLID to the original click, attributes the conversion, and updates reporting and Smart Bidding inputs.

The most common mistakes in step 5 are: uploading at checkout.session.completed instead of charge.succeeded (misses payment failures), uploading the gross amount instead of net of Stripe fees (depends on your accounting preference), and not handling currency conversion when Stripe charges and Google Ads accounts are in different currencies.

Capturing GCLID in Stripe customer or charge metadata

The first technical step is reliably capturing GCLID at click time and binding it to the financial event in Stripe. Three implementation patterns by complexity:

Pattern A — Stripe Checkout Session metadata (simplest):

When you create the Checkout Session server-side, pass the GCLID as a metadata field:

const session = await stripe.checkout.sessions.create({
  mode: "subscription",
  line_items: [{ price: "price_xyz", quantity: 1 }],
  metadata: {
    gclid: req.cookies.gclid || "",
    gbraid: req.cookies.gbraid || "",
    wbraid: req.cookies.wbraid || "",
    click_timestamp: req.cookies.click_ts || "",
  },
  success_url: "https://yoursaas.com/thanks",
  cancel_url: "https://yoursaas.com/pricing",
});

Pros: zero backend storage, metadata travels with the session forever. Cons: Stripe metadata values are limited to 500 chars each and the GCLID must be available at session-creation time (cookie set on landing page).

Pattern B — Customer object metadata (better for subscription renewals):

Store the GCLID on the Customer object on first signup. All future charges from that customer inherit the attribution:

const customer = await stripe.customers.create({
  email: req.body.email,
  metadata: {
    gclid: req.cookies.gclid || "",
    first_touch_campaign: req.cookies.utm_campaign || "",
    signup_date: new Date().toISOString(),
  },
});

Pros: works for recurring billing (every invoice.payment_succeeded inherits the customer GCLID). Cons: only captures first-touch GCLID, not last-touch — for SaaS with multiple ad-click touchpoints across a long sales cycle, you might want to update the GCLID on each new click.

Pattern C — Your own database, joined at webhook time:

Store GCLID in your own database keyed by user_id or session_id. When the Stripe webhook fires for charge.succeeded, look up the user's stored GCLID and pass it to Google Ads:

# In your charge.succeeded webhook handler:
user_id = stripe_customer.metadata.get('user_id')
gclid = db.query("SELECT gclid FROM users WHERE id = %s", [user_id])
if gclid:
    upload_to_google_ads(gclid, charge.amount, charge.currency, charge.created)

Pros: most flexible, supports last-touch attribution, supports multi-click attribution windows. Cons: requires backend infrastructure, more failure modes.

Best practice — capture all three iOS-era click IDs:

Modern Google Ads autotagging emits gclid for most traffic, plus gbraid for iOS App campaigns and wbraid for iOS web traffic when ATT-restricted. Capture all three. The Google Ads upload API has separate endpoints for each (UploadCallConversions, UploadClickConversions); use gclid where present, fall back to gbraid/wbraid for iOS traffic.

Configuring the Google Ads conversion action for value tracking

Before any upload code can succeed, you need to create the conversion actions in Google Ads that the uploads will target. In Tools > Conversions > New conversion action, choose "Import" as the source, then "Other data sources or CRMs" > "Track conversions from clicks."

Three conversion actions to create for a standard SaaS Stripe integration:

Action 1 — Stripe Trial Start (optional but recommended):

  • Category: Sign-up
  • Value: Don't use a value
  • Count: One
  • Click-through window: 90 days
  • View-through window: 1 day
  • Include in Conversions: No (initially — flip to Yes after first paid charge fires for the same user)

Action 2 — Stripe First Paid Charge (the primary action):

  • Category: Purchase
  • Value: Use different values for each conversion
  • Count: One (one conversion per click — the first paid charge)
  • Click-through window: 90 days
  • View-through window: 1 day
  • Include in Conversions: Yes (this is what Smart Bidding optimizes toward)
  • Attribution model: Data-driven (default in 2026)

Action 3 — Stripe Expansion Revenue (for upsells, plan upgrades, additional seats):

  • Category: Purchase
  • Value: Use different values for each conversion
  • Count: Every (multiple expansions per click can all count)
  • Click-through window: 90 days
  • View-through window: 1 day
  • Include in Conversions: Yes (additive to First Paid Charge in Smart Bidding)

For each conversion action, note the Conversion Action ID (a resource name like customers/1234567890/conversionActions/9876543210). You'll pass this to the upload API as conversion_action.

The most common single configuration error we see in Stripe-integrated Google Ads accounts is leaving multiple conversion actions flagged as "Include in Conversions" — both the old web pixel action and the new Stripe import action. Smart Bidding then double-counts: it credits the pixel conversion at checkout and the Stripe conversion at charge. Reported ROAS inflates 60-100%. The fix is one of: (a) flip the web pixel action's Include-in-Conversions to No, leaving Stripe Import as the sole optimization signal, or (b) keep both Include-in-Conversions but configure Smart Bidding's primary signal explicitly via custom goals.

Direct experience auditing 200+ SaaS Google Ads accounts

Setting "Use different values for each conversion" is critical for the Stripe revenue use case. Without it, every conversion uploads with the same fixed value (the "default value" you set on the action). Smart Bidding can't differentiate a €15/month basic plan signup from a €1,500/month enterprise signup if all conversions have the same value. Always select the "different values" option for Stripe-imported revenue actions.

Building the webhook listener (Node.js + Python examples)

The webhook listener has three responsibilities: verify the Stripe webhook signature (security), parse the event payload, and call the appropriate Google Ads API endpoint. Below are minimal implementations in Node.js and Python — both production-ready in shape but trimmed for clarity.

Node.js / Express implementation:

import express from "express";
import Stripe from "stripe";
import { GoogleAdsApi } from "google-ads-api";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const googleAds = new GoogleAdsApi({
  client_id: process.env.GOOGLE_ADS_CLIENT_ID,
  client_secret: process.env.GOOGLE_ADS_CLIENT_SECRET,
  developer_token: process.env.GOOGLE_ADS_DEVELOPER_TOKEN,
});
const customer = googleAds.Customer({
  customer_id: process.env.GOOGLE_ADS_CUSTOMER_ID,
  refresh_token: process.env.GOOGLE_ADS_REFRESH_TOKEN,
});

app.post("/webhooks/stripe", express.raw({ type: "application/json" }), async (req, res) => {
  const sig = req.headers["stripe-signature"];
  let event;
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  if (event.type === "charge.succeeded") {
    const charge = event.data.object;
    const gclid = charge.metadata.gclid;
    if (!gclid) return res.status(200).send("No GCLID — skipping");

    const conversionValue = charge.amount / 100; // Stripe amounts are in cents
    const conversionTime = new Date(charge.created * 1000).toISOString();

    await customer.conversionUploads.uploadClickConversions({
      conversions: [
        {
          gclid,
          conversion_action: process.env.GOOGLE_ADS_CONVERSION_ACTION_RN,
          conversion_date_time: conversionTime,
          conversion_value: conversionValue,
          currency_code: charge.currency.toUpperCase(),
        },
      ],
      partial_failure: true,
    });
  }
  res.status(200).send("OK");
});

Python / FastAPI implementation:

from fastapi import FastAPI, Request, HTTPException
import stripe
from google.ads.googleads.client import GoogleAdsClient

stripe.api_key = os.environ['STRIPE_SECRET_KEY']
client = GoogleAdsClient.load_from_storage('google-ads.yaml')

@app.post('/webhooks/stripe')
async def stripe_webhook(request: Request):
    payload = await request.body()
    sig = request.headers.get('stripe-signature')
    try:
        event = stripe.Webhook.construct_event(payload, sig, os.environ['STRIPE_WEBHOOK_SECRET'])
    except Exception:
        raise HTTPException(status_code=400, detail='Invalid signature')

    if event['type'] == 'charge.succeeded':
        charge = event['data']['object']
        gclid = charge['metadata'].get('gclid')
        if not gclid:
            return {'status': 'no gclid'}

        upload_service = client.get_service('ConversionUploadService')
        click_conversion = client.get_type('ClickConversion')
        click_conversion.gclid = gclid
        click_conversion.conversion_action = os.environ['GOOGLE_ADS_CONVERSION_ACTION_RN']
        click_conversion.conversion_date_time = (
            datetime.fromtimestamp(charge['created']).strftime('%Y-%m-%d %H:%M:%S+00:00')
        )
        click_conversion.conversion_value = charge['amount'] / 100
        click_conversion.currency_code = charge['currency'].upper()
        upload_service.upload_click_conversions(
            customer_id=os.environ['GOOGLE_ADS_CUSTOMER_ID'],
            conversions=[click_conversion],
            partial_failure=True,
        )
    return {'status': 'ok'}

Critical production hardening to add:

  • Idempotency: cache processed Stripe event IDs for 24h, skip duplicate events
  • Exponential backoff retry on Google Ads API 5xx errors (3 retries: 1s, 5s, 25s)
  • Dead-letter queue for permanently failed uploads (review weekly)
  • Structured logging of every upload (charge_id, gclid, conversion_value, response_code)
  • Alerting when error rate exceeds 1% on a rolling 1-hour window

Currency handling, refunds, and conversion adjustments

The two operational headaches that derail most Stripe-to-Google integrations are currency normalization and refund handling. Both must be solved before Smart Bidding will work correctly.

Currency normalization:

Google Ads accounts have a single currency set at creation. All uploaded conversion values must be in that currency. Stripe accounts can charge in 135+ currencies. The reconciliation logic:

  1. If Stripe charge.currency matches Google Ads account currency → use charge.amount directly (after dividing by 100 for the cents-to-units convention)
  2. If Stripe charge.currency differs → use the Stripe BalanceTransaction's amount and exchange_rate to convert
  3. If BalanceTransaction isn't yet available (race condition right after charge.succeeded) → query an external FX rate API at the charge timestamp

Pseudocode for the safe path:

def normalize_currency(charge, target_currency):
    if charge['currency'].upper() == target_currency:
        return charge['amount'] / 100
    # Fetch balance transaction (settled in your Stripe payout currency)
    bt = stripe.BalanceTransaction.retrieve(charge['balance_transaction'])
    if bt['currency'].upper() == target_currency:
        return bt['amount'] / 100
    # Both differ — convert via Stripe's exchange rate
    return (bt['amount'] / 100) * bt['exchange_rate']

Refund handling via UploadConversionAdjustments:

When Stripe fires charge.refunded, you must adjust the corresponding Google Ads conversion. The adjustment API accepts two operations:

  • RETRACT: removes the original conversion entirely (use for full refunds)
  • RESTATE: changes the conversion value (use for partial refunds — pass the new, lower value)

Both operations require: original GCLID, conversion action resource name, original conversion date-time (must match exactly), and the adjustment timestamp.

// charge.refunded handler
const refund = event.data.object;
const isFullRefund = refund.amount_refunded === refund.amount;

await customer.conversionAdjustmentUploads.uploadConversionAdjustments({
  conversionAdjustments: [
    {
      gclid_date_time_pair: {
        gclid: refund.metadata.gclid,
        conversion_date_time: originalConversionTime, // must match original exactly
      },
      conversion_action: process.env.GOOGLE_ADS_CONVERSION_ACTION_RN,
      adjustment_type: isFullRefund ? "RETRACTION" : "RESTATEMENT",
      adjustment_date_time: new Date().toISOString(),
      restatement_value: isFullRefund
        ? undefined
        : {
            adjusted_value: (refund.amount - refund.amount_refunded) / 100,
            currency_code: refund.currency.toUpperCase(),
          },
    },
  ],
});

The 55-day adjustment window: Google Ads only accepts adjustments within 55 days of the original conversion timestamp. Refunds beyond 55 days cannot be reflected in Google Ads. For SaaS with long refund windows (90-day money-back guarantees, annual subscription mid-year refunds), this is a structural gap — you'll need to manually reconcile in Looker / GA4 reporting, and accept that Smart Bidding is operating with slightly stale numbers on a small percentage of conversions.

Disputes and chargebacks: subscribe to charge.dispute.created. If reason is "fraudulent" or "credit_not_processed," treat as full refund (RETRACT). If "duplicate" or "subscription_canceled," follow your own business rules — generally also treat as full refund for ad attribution purposes.

BigQuery middleware option for high-volume accounts

For SaaS doing more than 10,000 conversions/month or running multi-product, multi-region Google Ads accounts, the direct-webhook pattern starts straining. The BigQuery middleware pattern adds 5 minutes latency but solves several problems at once.

The architecture:

  1. Stripe webhooks → Cloud Function (Node.js or Python) → BigQuery raw events table
  2. BigQuery scheduled query (every 5 minutes) → reads new events, computes conversion records, calls Google Ads API
  3. BigQuery materialized views → reconciliation dashboards, refund tracking, attribution debugging

The Cloud Function does almost nothing — just validates the Stripe signature and writes the raw event to BigQuery. The scheduled query holds all the conversion logic, which means you can update logic by editing SQL rather than deploying code.

Schema for the raw events table:

CREATE TABLE stripe_events (
  event_id STRING NOT NULL,
  event_type STRING NOT NULL,
  charge_id STRING,
  customer_id STRING,
  gclid STRING,
  gbraid STRING,
  wbraid STRING,
  amount_cents INT64,
  currency STRING,
  occurred_at TIMESTAMP NOT NULL,
  processed_at TIMESTAMP,
  google_ads_upload_status STRING, -- pending / success / failed
  google_ads_upload_response JSON,
  raw_payload JSON
)
PARTITION BY DATE(occurred_at)
CLUSTER BY event_type, gclid;

The scheduled query runs every 5 minutes:

-- Pseudocode — actual implementation uses Cloud Function callable from BQ
INSERT INTO conversion_uploads (event_id, gclid, conversion_value, status)
SELECT
  event_id,
  gclid,
  amount_cents / 100.0 AS value,
  upload_to_google_ads(gclid, conversion_action_rn, occurred_at, amount_cents / 100.0, currency) AS status
FROM stripe_events
WHERE event_type = 'charge.succeeded'
  AND google_ads_upload_status = 'pending'
  AND gclid IS NOT NULL
  AND occurred_at > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 80 DAY); -- within GCLID window

Why the BigQuery middleware pattern wins at scale:

  • Replay capability: if Google Ads API has an outage, raw events stay in BigQuery and can be re-uploaded later
  • Auditable ledger: every upload's status, timestamp, and response is queryable
  • Cheaper at scale: batched API calls cost less than per-event calls (Google Ads API has rate limits and quotas)
  • Multi-account routing: route conversions to the right Google Ads account based on Stripe product or region — easy in SQL, complex in webhook code
  • Reconciliation dashboards: BigQuery → Looker dashboards showing Stripe revenue vs Google Ads imported revenue, refund lag, currency mismatches

The downside: 5-minute latency vs sub-second for direct webhooks. For Smart Bidding's learning loop, 5-minute latency is invisible — Smart Bidding updates daily, not in real time.

For deeper BigQuery pipeline patterns specific to ads data, see our BigQuery Google Ads data pipeline tutorial.

30-day implementation plan with rollout checkpoints

The HowTo schema above is the day-by-day. Strategic framing for the 30-day rollout:

Week 1 — Audit and design (Days 1-7). Document the current state: how conversions currently fire, what the reported ROAS is, what the actual Stripe revenue is for the same window. The gap is what you're about to fix. Define the conversion taxonomy (Trial Start, First Paid, Expansion). Create the Google Ads conversion actions but don't include them in Conversions yet — they're inactive until data flows.

Week 2 — Implementation (Days 8-15). Add GCLID capture to landing pages. Wire Stripe Checkout to pass GCLID as metadata. Build the webhook listener for charge.succeeded. Test against a Google Ads test account with synthetic GCLIDs. Validate that test conversions appear in the Google Ads Uploads tab within 5 minutes.

Week 3 — Hardening (Days 16-22). Add currency normalization. Wire charge.refunded and charge.dispute.created webhooks to UploadConversionAdjustments. Add idempotency, retry logic, and dead-letter queue. Stand up the Stripe-vs-Google reconciliation dashboard. Run it for 7 days against live data without yet switching Smart Bidding to the new conversion action.

Week 4 — Switchover and tuning (Days 23-30). Flip "Include in Conversions" on the Stripe First Paid Charge action. Flip it off on the old web pixel action. Switch Smart Bidding's primary conversion to Stripe First Paid Charge. Expect 14-30 days of bid volatility as the algorithm rebuilds its model. Don't change target ROAS during this period; let it stabilize, then recalibrate.

Rollout checkpoints to monitor:

  • End of week 1: conversion actions exist in Google Ads, taxonomy documented, baseline ROAS measured
  • End of week 2: test conversions visible in Google Ads Uploads tab, no errors in webhook logs
  • End of week 3: live conversions flowing, reconciliation dashboard within 5% tolerance, refunds processing as RETRACT/RESTATE correctly
  • End of week 4: Smart Bidding switched, learning period begun, team trained on the new dashboards

Beyond day 30 — ongoing operations:

Run a quarterly attribution audit. Compare Google Ads reported revenue to Stripe collected revenue for the same window. The two should agree within 5%; a larger gap indicates GCLID capture failures, refund handling bugs, or attribution-window expiries. For accounts under €50k/month, this can be a 1-hour task each quarter. For accounts above €100k/month, build it into the BigQuery middleware as automated weekly reconciliation reports.

If you're managing Google Ads at scale and want AI-driven optimization layered on top of clean Stripe-imported conversion data, SteerAds runs a free 14-day audit on your Google Ads + Microsoft Ads accounts including a check of your conversion import health.

For broader context on attribution and conversion measurement, see our attribution data-driven vs last-click 2026 guide and the Google Ads conversion tracking server-side 2026 guide for adjacent implementation patterns.

Sources

Official and third-party sources consulted for this guide:

Related reading: AI image generation for Google Ads 2026: Midjourney, DALL-E, and ad creative · BigQuery + Google Ads data pipeline 2026: build your reporting warehouse · Consent Mode v2 implementation 2026: mandatory EEA setup for Google Ads + GA4 · Dynamic Creative Optimization in Google Ads 2026: DCO setup and strategy · Enhanced Conversions for Google Ads 2026: recover 5-15% lost attribution · GA4 + Google Ads conversion import setup 2026: complete 30-day implementation guide

FAQ

Do I need to import Stripe revenue if I already use Google Ads Enhanced Conversions for Web?

Yes, in most cases. Enhanced Conversions for Web fires at checkout completion based on a client-side event — it captures intent-to-pay, not actual collected revenue. If you have free trials, dunning failures, refunds, payment plans, or any time delay between checkout and successful charge, Web Enhanced Conversions will over-report. Importing Stripe charge.succeeded events as offline conversions gives Google Ads the true collected revenue figure, which dramatically improves Smart Bidding ROAS targeting. The two systems complement each other: Web Enhanced Conversions for fast intent signal, Stripe offline import for ground-truth revenue. Run both, but configure Smart Bidding to optimize toward the Stripe-imported value action, not the Web action.

What happens if a customer's GCLID is older than 90 days when they finally convert?

Google Ads has hard cutoffs for offline conversion uploads: the GCLID must have been generated within the last 90 days for Search/Display, or 30 days for YouTube. Beyond that window, the upload silently fails with no error in the Conversions report. For SaaS with long sales cycles, this is the single biggest source of underreported revenue. Workarounds: (1) For free trials longer than 60 days, fire the conversion at trial start (paid trial conversion) and adjust later if the trial doesn't convert to paid — Google's adjustment API supports retraction. (2) For genuinely long cycles (B2B sales 90+ days), supplement with Enhanced Conversions for Leads using hashed email instead of GCLID — the match window extends to 540 days but match rate is lower (typically 40-65%).

How do I handle multi-currency Stripe charges when Google Ads only accepts one account currency?

Convert all charges to the Google Ads account currency at the time of upload using the FX rate on the charge.succeeded timestamp. Stripe's API returns the amount in the original currency (charge.currency) and the converted amount in your Stripe account's settlement currency (balance_transaction.amount). Use the settlement-currency value if the Google Ads account is set to your settlement currency. If they differ, query a rate API (Stripe doesn't expose its rate directly) at the charge timestamp — use Stripe's own balance_transaction.exchange_rate when available. Document the conversion logic; Smart Bidding will misoptimize if half your conversions are EUR-native and half are USD converted at stale rates.

Do I really need to handle Stripe refunds via the conversion adjustment API?

Yes, and it's the most-skipped step in Stripe-to-Google integrations. If you ignore refunds, your Google Ads ROAS metric becomes inflated by the gross revenue figure, and Smart Bidding will scale spend on campaigns that look profitable on gross but are unprofitable net of refunds. The Google Ads Conversion Adjustment API supports two operations: RETRACT (full refund — removes the conversion entirely) and RESTATE (partial refund — adjusts the value). Wire a Stripe charge.refunded webhook to the adjustment endpoint. Process refunds within 55 days of the original conversion (Google's adjustment window); beyond that, you'll have to manually reconcile in reporting and accept that bidding is using stale numbers.

Should I use the Google Ads API directly or a third-party tool like Zapier / Funnel.io?

Three buckets. (1) Below 500 conversions/month: Zapier/Make works fine — pre-built Stripe-to-Google-Ads templates exist, no engineering required, cost ~€30-80/month. (2) 500-10,000 conversions/month: direct Google Ads API integration is worth it. Better error handling, faster latency (Zapier batches every 15 minutes), cheaper at scale, and you can implement the refund adjustment logic which most pre-built tools skip. Engineering cost: 2-4 days. (3) 10,000+ conversions/month or multi-product accounts: BigQuery middleware pattern (covered in section 7). Stripe webhooks land in BigQuery, scheduled query upserts to Google Ads API. Adds 5 minutes latency but gives you a queryable historical ledger of every conversion sent.

What's the difference between offline conversion imports and Google Click ID-based conversion goals?

They're the same mechanism, different terminology. 'Offline conversion import' is the operational term for sending GCLID + conversion value + timestamp to Google Ads via API or file upload. 'Conversion goals' (or 'conversion actions') is how those imports are categorized in the Google Ads UI — each unique conversion type (trial signup, paid signup, expansion revenue, etc.) gets its own conversion action. For Stripe revenue, most SaaS create three actions: Stripe Trial Start (no value, intent signal), Stripe First Paid Charge (revenue value), Stripe Expansion Revenue (value of upsells/upgrades). Configure Smart Bidding to optimize toward Stripe First Paid Charge specifically — that's the bottom-line signal.

How do I troubleshoot when conversions aren't appearing in Google Ads after upload?

Check in this order. (1) Look at the Conversions > Uploads tab in Google Ads — it shows the last 90 days of upload jobs and error counts. Common errors: 'GCLID not found' (the click predated upload window or never existed), 'Conversion action not found' (wrong action ID), 'Duplicate conversion' (re-upload of same GCLID + timestamp). (2) If uploads succeed but conversions don't show in reports, check the conversion action's attribution window and lookback. (3) Verify the GCLID was actually captured at click time — fire a test click on one of your own ads, check Stripe metadata after checkout. (4) Confirm the conversion action is set to 'Include in Conversions' — actions exist in two states and only 'included' ones flow to Smart Bidding.

What's the impact on Smart Bidding when I switch from web-pixel conversions to Stripe-imported conversions?

Significant, both up and down. Smart Bidding re-learns when the conversion signal changes, which means 14-30 days of bid volatility while it adapts. During that window, expect 10-20% spend variance and possibly 15-25% CAC variance — this is normal. After the learning period, accounts typically see ROAS reported in Google Ads drop 20-40% (because pixel-based conversions over-reported gross intent, Stripe-imported conversions report net collected revenue). The absolute revenue doesn't change — only the reported ratio. Don't panic and revert; the new number is closer to truth. Use this as the opportunity to recalibrate target ROAS in Smart Bidding to match your true unit economics, not the inflated pixel numbers.

💡

Get our best tips to cut your CPA

Each week, an actionable tip to optimize your Google & Bing Ads campaigns. Joined by 1,200+ advertisers.

No spam. One-click unsubscribe. Privacy policy.

Ready to optimize your campaigns?

Start a free audit in 2 minutes and discover the ROI potential of your accounts.

Start my free audit

Free audit — no credit card required

Keep reading