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
| Event | When | Order ID |
|---|---|---|
purchase | A first paid checkout with no trial | The checkout's bb_event_id, the same transaction_id the browser sent, so Google keeps one of the two |
trial_converted_to_paid | The first non-zero paid invoice after a trial | The Stripe subscription id: a subscription converts once |
subscription_renewed | Every later paid invoice | The Stripe invoice id |
refund | A charge refunded, fully or in part | Its 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:
| Reason | Meaning |
|---|---|
no_click_id | No live ad click on record for this customer |
consent_denied | The buyer refused ad consent at checkout |
internal_account | The buyer matched your internal-account list |
fake_click_id | A test value such as SETUP-CHECK-... was removed |
no_original_conversion | A 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
- 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. - In the console, open Settings › Tracking & Tags › Server-side conversions. Under Conversion actions, type that name exactly for each event you want exported.
purchaseandtrial_converted_to_paiddefault toPaid subscription; renewals stay out until you name an action for them. - Click Create token. It is shown once; we keep only a hash.
- 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_1Q2w3ETimes 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.
Consent and your privacy notice
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
| Method | Path | What |
|---|---|---|
GET | /api/organizations/tracking/ad-conversions | The ledger, newest first. status, event, page, limit |
GET | /api/organizations/tracking/ad-conversions/summary | Counts, settings, export URLs |
PATCH | /api/organizations/tracking/ad-conversions/settings | Conversion names, exclusions, Google Ads |
POST | /api/organizations/tracking/ad-conversions/token | Rotate the export token |
GET | /api/v1/public/:orgId/ad-conversions/google-ads.csv | The import, token-protected |
GET | /api/v1/public/:orgId/ad-conversions/google-ads-adjustments.csv | Refund adjustments, token-protected |
Meta and LinkedIn offline formats are planned on the same ledger.