BuildBaseBuildBase

Conversions without a browser

Trial conversions, renewals and refunds happen in a Stripe webhook with nobody on your site. BuildBase records each one against the original ad click and hands it to Google Ads as an offline import.

A seven-day trial converts to paid on day seven, from a Stripe webhook, with nobody on the site. So does every renewal and every refund. None of these can ever be a browser event, so no tag can report them - and the conversion your ad platform most needs to see, the paying customer, is the one it never hears about.

BuildBase closes that gap on the server. The SDK already holds the click id the visitor arrived with; it now also stores it on the user at sign-up and on the workspace when it is created. When Stripe reports a payment, the webhook handler writes a row to the org's ad_conversions ledger, joined to that click. You then deliver the ledger to Google Ads, as a scheduled import today or through the Google Ads API where it is enabled.

What gets recorded

EventWhenOrder ID
purchaseA first paid checkout with no trialThe checkout's bb_event_id, the same transaction_id the browser sent, so Google keeps one of the two
trial_converted_to_paidThe first non-zero paid invoice after a trialThe Stripe subscription id: a subscription converts once
subscription_renewedEvery later paid invoiceThe Stripe invoice id
refundA charge refunded, fully or in partIts own id; it adjusts the order it reverses

Each row carries the org and workspace, the event id, the click id and its type (gclid, gbraid, wbraid, fbclid and the rest), when the click was captured, the conversion time, the value in major units (negative for a refund), the currency, a SHA-256 hash of the buyer's normalised email (never the address), the consent answer from checkout, and a status.

A Stripe event delivered twice writes one row. A row that cannot be sent is still written, with the reason, so the numbers add up:

ReasonMeaning
no_click_idNo live ad click on record for this customer
consent_deniedThe buyer refused ad consent at checkout
internal_accountThe buyer matched your internal-account list
fake_click_idA test value such as SETUP-CHECK-... was removed
no_original_conversionA refund with no recorded sale to adjust

Click ids expire 90 days after the click, which is the window Google accepts. The time is measured from when the SDK captured the landing, not from when the row was written.

Note

Needs @buildbase/sdk 0.0.72 or later for the capture time and for storing attribution at sign-up and workspace creation. With an older SDK, rows still use the click ids stamped on the checkout itself.

Deliver to Google Ads: scheduled import

  1. In Google Ads, create a conversion action of type Import > Other data sources or CRMs > Track conversions from clicks. Name it, for example, Paid subscription.
  2. In the console, open Settings › Tracking & Tags › Server-side conversions. Under Conversion actions, type that name exactly for each event you want exported. purchase and trial_converted_to_paid default to Paid subscription; renewals stay out until you name an action for them.
  3. Click Create token. It is shown once; we keep only a hash.
  4. In Google Ads, go to Goals › Conversions › Uploads › Schedules, add a source of type HTTPS, paste the Conversions URL, use any username and the token as the password, and pick a schedule.

The file follows Google's offline click conversion format:

Google Click ID,Conversion Name,Conversion Time,Conversion Value,Conversion Currency,Order ID
Cj0KCQjw...,Paid subscription,2026-09-20 09:05:07+0000,49.00,USD,sub_1Q2w3E

Times are UTC. Each fetch returns every live row from the last 90 days; Google ignores order ids it has already imported, so a missed run loses nothing.

Refunds are a second file, the Refund adjustments URL, because Google's click import accepts no negative value. A full refund retracts the original conversion and a partial one restates it at what is left. Schedule it the same way as a conversion adjustments upload.

The export also answers ?token= for anything that cannot send Basic auth. A wrong or missing token is a 401.

Deliver to Google Ads: the API

Direct upload through uploadClickConversions is built, and behind a platform flag until it is switched on for your organization. Where it is on, the same tab shows a Direct upload card: your customer ID, a refresh token from the Google Ads OAuth consent, and the numeric id of each conversion action. Uploads use partial failure, so one bad row does not block the rest; a refused row keeps Google's message, and is retried up to five times.

Internal accounts

Your own sign-ups are not customers. Under Internal accounts, list email domains, addresses and workspace ids. Their conversions still appear in the ledger, marked internal_account, and never reach any ad vendor, including the Meta and GA4 server-side sends.

Remove test click ids clears values such as SETUP-CHECK-... from stored attribution and marks any queued row that carried one.

A row is only exported when the buyer's consent answer allowed marketing, or when no answer was recorded. Whether uploading conversions for customers who were never asked is lawful for you, and on what basis, is your decision; state it in your privacy notice. The ledger holds a hashed email and a click id, and rows are deleted after a year.

API

MethodPathWhat
GET/api/organizations/tracking/ad-conversionsThe ledger, newest first. status, event, page, limit
GET/api/organizations/tracking/ad-conversions/summaryCounts, settings, export URLs
PATCH/api/organizations/tracking/ad-conversions/settingsConversion names, exclusions, Google Ads
POST/api/organizations/tracking/ad-conversions/tokenRotate the export token
GET/api/v1/public/:orgId/ad-conversions/google-ads.csvThe import, token-protected
GET/api/v1/public/:orgId/ad-conversions/google-ads-adjustments.csvRefund adjustments, token-protected

Meta and LinkedIn offline formats are planned on the same ledger.