Deep dive · 11 min read

How to import order margin into Google Ads when your shop doesn't store the click ID

Your shop platform doesn't store the Google Click ID with each order. Take it from the GA4 export to BigQuery and send order margin to Google Ads by API.

A chef plates a small dish on a plate labelled Margin, while a cook behind him holds an overflowing plate labelled Revenue that is being turned away. A small robot waiter labelled Google Ads waits at the counter.
The serious facts are real — the article covers are not.

In short: The usual API import of orders into Google Ads needs the Google Click ID stored with each order, and a boxed shop platform often doesn't store it. You don't need to store it in the shop. The GA4 export to BigQuery already holds the click with each visit that came from an ad, so you match orders to clicks in BigQuery. Then you send the margin of every consented order through the Data Manager API, with the click ID or a hashed email address and phone number, and send cancellations through the Google Ads API. Nothing changes on the website.

Free download

Want the brief for Claude Code right away?

Claude Code or a developer writes the whole import from this instructions package. Leave your email and the file is yours, or keep reading.

One file, one list. I only write when there's something worth reading.

Google Ads optimises campaigns on the revenue reported by the website tag. Revenue is not profit. Most shops sell their own private-label products next to goods from suppliers, and in my experience their margins range roughly from 20 to 60%. When bidding uses revenue, two orders with the same revenue count the same, even if one has a low margin and the other a high one. Typically 3 to 10% of orders also get cancelled. The website tag does not record cancellations, so the conversion keeps its full value.

You don’t want to send margin through the website tag, because anyone can read the tag’s data in the browser, including your competitors. That is why margin goes to Google Ads through the API.

The usual API import needs the Google Click ID of each order. On a boxed platform such as Shopify or Shoptet, you don’t control how the checkout saves an order, and the order has no click ID field. Shopify, for example, keeps only the landing page of the customer’s first and last visit. That page may contain the click ID, but only if the visitor allowed tracking. Without custom code or an app that captures it, the order won’t reliably carry the click ID. You don’t need the click ID in the shop. The GA4 export to BigQuery already stores the click with each visit that came from an ad.

Key takeaways tl;dr
  • What exactly to send. For each order you send the order number, the margin as the value, the time, the click, the hashed email address and phone number, consent, and whether the customer is new or returning.
  • Cancellations go through the older Google Ads API, because the newer Data Manager API cannot withdraw a conversion. New orders and margin changes go through the Data Manager API.
  • Send every order with consent, not only those with a click. Orders without a Google Click ID go with the hashed email address and phone number, and Google matches part of them through enhanced conversions.
  • The click comes from the GA4 export to BigQuery, so nothing new gets stored in the shop. The export is free, and BigQuery costs a few dollars a month for a typical shop.

What you need

  • GA4 on the shop’s website, recording purchases with the order number
  • the shop’s orders with purchase prices, from an order export, the platform’s API or the database
  • a Google Cloud project
  • admin access to the Google Ads account
  • a place to run one job a day

Nobody has to change the website or the checkout. The click comes from GA4, which already runs on the site, and the margin comes from order data the shop already keeps. Boxed platforms usually export orders with purchase prices, but check that yours does.

Step 1: Create the Google Cloud project

One Cloud project contains the GA4 export and gives access to both APIs. If GA4 already exports to a project, use that one.

  1. If you have no billing account yet, open Billing in the Google Cloud console and create one. A new billing account starts as a Free Trial.
  2. Open the billing account’s Overview page. If it says Free trial account, click Activate there or on the console’s Welcome page and confirm in the dialog.
  3. Open Manage resources, click Create project, enter a name and choose that billing account. On a personal account the organisation stays at No organization. Click Create.
  4. In APIs & Services, open Library, find BigQuery API and click Enable. Do the same for Data Manager API and Google Ads API.

The project needs a paid billing account. Google currently documents that it rejects Google Ads API access requests from projects on Free Trial or with a suspended or disabled billing account. Without a billing account, BigQuery runs as a sandbox, where tables expire after 60 days.

The GA4 export itself is free. BigQuery charges for storage and queries, which in my experience comes to a few dollars a month for a typical shop. To see the real amount, open Billing, select the billing account, open Reports and filter to this project.

Check. The Enabled APIs & services page lists all three APIs.

Step 2: Turn on the GA4 export to BigQuery

In this step you get the Google Click ID without changing anything in the shop. Do it early, because the export contains only the days after you link it. Google’s help describes no way to export earlier days.

You need the Editor role on the GA4 property and the Owner role on the Cloud project. In Google Ads, check that auto-tagging is on, under Admin, Account settings, Auto-tagging. Without it, ad clicks carry no Google Click ID. If it is off, tick Tag the URL that people click through from my ad and click Save.

  1. In GA4, open Admin. Under Product links, click BigQuery links, then Link.
  2. Click Choose a BigQuery project, select the project from Step 1 and click Confirm.
  3. Choose the data location. For a shop in the EU, choose a European Union location (the EU multi-region or an EU region) and note it, because the import’s queries and state tables use the same location. If the project already has a dataset for this property, the location is fixed and can’t be changed. Click Next.
  4. Click Configure data streams and events, select the shop’s web stream and click Done.
  5. Leave Include advertising identifiers for mobile app streams off. A web shop doesn’t need it.
  6. Choose Daily and leave Streaming off.
  7. Click Next, then Submit.

Daily is enough. The import reads the daily tables events_YYYYMMDD. Streaming adds today’s data within minutes, which a daily job doesn’t use, and Streaming costs extra.

The 1 million limit. A standard GA4 property can export 1 million events a day with Daily. If the property keeps going over it, Google pauses the export and does not fill in the missing days. If you are close, exclude events you don’t need, such as scroll or user_engagement, in the data streams window. Keep purchase, page_view and session_start, which carry the order, the landing page and the click. If that is not enough, Streaming has no limit but needs the import adapted, and Google’s help also suggests an upgrade to Analytics 360.

The first weeks. Clicks from before the link date are not in BigQuery. For the first 30 days, the import therefore sends more orders with only the hashed email address and phone number.

Check. Data should start arriving within 24 hours, in a BigQuery dataset analytics_<property ID>. Each day a table with the previous day’s data is added, usually in the early afternoon in the property’s time zone, sometimes later in the day or the next day.

Step 3: Set up access to both APIs

New orders and margin changes go through the Data Manager API. Cancellations go through the older Google Ads API, because the Data Manager API cannot withdraw a conversion. Both use the same project and the same sign-in.

I have written code against the Google Ads API for twelve years, and in 2026 its access changed. Google discontinued developer tokens on September 9, 2026, and access now belongs to the Cloud project. A new project starts with the Test access level, which works only with test accounts. Your real account needs Explorer.

  1. In the Cloud console, open the Google Ads API Overview page and check that the access level is Test.
  2. Expand Upgrade access level and check that the next level is Explorer.
  3. Click Apply for access, fill in the short form and wait for Google’s answer by email. The Overview page shows Explorer only after approval.

Explorer allows 2,880 operations a day in live accounts and needs no brand verification. Google approves it automatically in most cases. One documented cause of a rejection is a Free Trial or a suspended or disabled billing account, so check the reason in the email. Fix the billing account, wait a few minutes and apply for Explorer again.

The sign-in. For a user account, set up OAuth in the Cloud console under Google Auth Platform.

  1. On Branding, fill in the app name, a user support email and a developer contact email, then click Create.
  2. On Data Access, click Add or remove scopes and add Data Manager API, Google Ads API and BigQuery API, because the import uses the same sign-in to read the GA4 export. Click Update, then Save.
  3. On Audience, choose Internal only if the Cloud project belongs to your Google Workspace organisation and every user is in it. Otherwise choose External and click Publish app, so the status is In production.

Never leave the status at Testing, even though Google’s Data Manager guide uses the Testing status for development. Credentials from a sign-in in Testing expire after 7 days, so the daily run stops after a week and nobody is told unless the job alerts on failure. A published app that Google hasn’t verified shows an unverified app warning once at the first sign-in. For your own use, click Advanced and continue, because the warning doesn’t mean anything is wrong.

Create the credentials.

  1. If the import will run on a server or your own computer, create an OAuth client of type Desktop app on Clients and download its JSON. Google Apps Script signs in through the script itself and does not need this file.
  2. Give the JSON to whoever builds the import (Step 5). That person uses the JSON to sign in once. This produces stored credentials, which the import renews automatically.

On a server, a service account works instead. Create it, grant it Service Usage Consumer and BigQuery Job User on the project, BigQuery Data Viewer on the GA4 dataset and BigQuery Data Editor on the import’s own dataset, then add its email in Google Ads as below.

Then add the account in Google Ads under Admin, Access and security, with the plus button and Standard access. For a person, click Send invitation, and the person accepts it. For a service account, enter its email and click Add account, with no invitation. Use an account that stays with the company, because the import stops when it is deleted.

Check. The Overview page shows Explorer. Step 6 tests both APIs, with a validateOnly call to Data Manager and a test cancellation call to the Google Ads API.

Step 4: Create the conversion action for margin

Create a separate conversion action of the import type for margin. Here you also turn on enhanced conversions, which let Google match orders without a click.

  1. In Google Ads, click Goals, then Conversions, then Summary.
  2. Click + Create conversion action and select Conversions offline.
  3. When asked for a data source, choose Skip this step and set up a data source later. The import sends the data through the API.
  4. Then define the action itself in the wizard. Choose the Purchase category, name the action, click Save and continue and finish with Agree and finish.

Google’s help says a new offline action isn’t active until a data source is connected. In practice the API upload is that source, and the ten-order test in Step 6 is the proof. If the action shows no value the next day, open the action, click Edit data sources and check them.

Open the action, click Edit settings and set these:

  • Value to Use different values for each conversion. Otherwise Google uses the default value instead of your margin. The field still requires a default value, so enter any small one, which Google uses only when an event carries no value.
  • Count to Every, because each order is a separate sale.
  • Click-through conversion window to 90 days, so orders whose click came weeks earlier still count.
  • Attribution model to Data-driven where it is offered. Select it or keep it if it is already set.

Click Save. Then, in Summary, open the row with the margin action and click Edit goal. Under Conversion action optimization, choose Secondary (observe only) and click Save, so the margin doesn’t affect bidding before you check it. A campaign with a custom goal uses every action in that goal for bidding, including secondary ones, so keep the margin action out of custom goals.

For enhanced conversions, click Goals, then Settings, expand Customer data use, tick Turn on enhanced conversions and click Agree and save. Then review the customer data terms and click Agree and continue. If you don’t accept these terms, Google doesn’t use the hashed email address and phone number.

Copy the action’s ID from its Details tab, next to Conversion type ID. Also note the 10-digit ID of the Google Ads account that owns the action. Google recommends waiting 4 to 6 hours before the first upload, so the next day is a convenient time for the first send.

Check. The action’s conversion source reads Website (Import from clicks), and enhanced conversions are ticked under Customer data use.

Free download

Have the code written from the instructions package

The click-matching query, the margin calculation and the daily corrections are in one file for Claude Code or a developer. Leave your email and the file is yours.

One file, one list. I only write when there's something worth reading.

Step 5: Find the ad click for each order in BigQuery

From here on, Claude Code or a developer builds the import from the download package. The steps below say what the import does and what you check.

This step replaces storing the click in the shop. You match each order to its visit in the GA4 export.

Find the GA4 purchase whose ecommerce.transaction_id equals the shop’s order number, compared as text, with only the spaces at the start and end removed. A visit is identified by the pair of user_pseudo_id and ga_session_id. Take the latest Google Click ID or wbraid that was recorded during the purchase visit, before the purchase. If the visit has none, find the same visitor’s earlier visits by user_pseudo_id, up to 30 days before the purchase, and take the latest click identifier from them. If one event carries both, use the Google Click ID.

The Google Click ID is in collected_traffic_source.gclid. wbraid is the iPhone click identifier. If the exported page URL in page_location keeps the wbraid parameter, the query reads it from there, so check this on your data.

The full query, including events that arrive late, is in the instructions package.

Check. Take five orders from the shop that have a GA4 purchase within the exported days. The query must find that purchase for each, and the order numbers must match character by character. The same query shows for how many purchases it finds a click.

Step 6: Send the orders with margin to the Data Manager API

In this step you send the full set of data for each order through the Data Manager API, with the method events:ingest. One request can carry up to 2,000 orders.

The margin is the gross margin of the order without VAT. Take the item prices after item discounts, multiplied by the number of units the customer kept. Subtract any order discount and the purchase prices of those units. Leave out shipping and payment fees. Don’t treat a missing purchase price as zero. Don’t send such an order until it has purchase prices.

Each event contains these fields:

  • transactionId, the order number
  • conversionValue and currency, the margin and its currency
  • eventTimestamp, the order time with a time zone offset
  • eventSource, WEB
  • adIdentifiers, the Google Click ID or wbraid from Step 5
  • userData, the hashed email address and phone number
  • consent, with adUserData granted
  • userProperties.customerType, NEW or RETURNING, if the shop’s earlier orders show it
{
  "transactionId": "ORDER-EXAMPLE-000",
  "eventTimestamp": "2026-01-01T12:00:00+01:00",
  "eventSource": "WEB",
  "conversionValue": 123.45,
  "currency": "EUR",
  "adIdentifiers": { "gclid": "INVALID-EXAMPLE-CLICK-ID" },
  "userData": {
    "userIdentifiers": [
      { "emailAddress": "sha256-hex-of-the-normalised-email" },
      { "phoneNumber": "sha256-hex-of-the-e164-phone-number" }
    ]
  },
  "consent": { "adUserData": "CONSENT_GRANTED" },
  "userProperties": { "customerType": "NEW" }
}

All values in the example are made up. The example is one event, not a full request. The import puts it into a request together with the destination (the ID of the account that owns the action and the action ID) and the encoding. Normalise the email address and phone number by Google’s formatting rules, hash them with SHA-256 and set the request’s encoding to HEX.

Send every order with consent, not only those with a click. The import sends every eligible order with consent and at least one identifier (click ID, email address or phone number). It sends an order without a click with its hashed email address, phone number or both, and Google matches part of these orders through enhanced conversions, not all. You can’t tell in advance which ones. An order that has no click ID, no hashed email address and no hashed phone number can’t be matched, so you skip it.

Consent. Send an order only when ad_user_data consent has been recorded for it, and never send missing consent as denied. The GA4 export carries only privacy_info.ads_storage. Use it only if your cookie banner always grants ad_storage and ad_user_data together, which you check in Tag Assistant. Otherwise take consent from the shop’s own records. Whether that is legally sufficient is for the shop to assess.

The first send. The import can send an order once the GA4 daily table for the order’s day exists. That is only the first condition, and the import also applies the waiting rules from the package.

  1. Send one batch to Data Manager with validateOnly set to true, and one cancellation call to the Google Ads API with validateOnly too. Google checks them and stores nothing.
  2. Send ten real orders, preferably recent ones with a Google Click ID. The next day, check that the request finished with SUCCESS and that the margin action shows their value in “All conv. value”. Secondary actions don’t appear in the standard Conversions columns, and the values are in the account currency. If you can cancel one of these ten orders, cancel it too (Step 7).
  3. Then turn on the daily schedule with alerts on failure.

A response from events:ingest only means that Google accepted the request. The import keeps each request ID and reads its result on the next run (Google needs 30 minutes to 24 hours).

Step 7: Send margin changes and cancellations every day

Every day the import compares each sent order with its state in the shop.

Margin change. Send the stored event to the Data Manager API again with the same order number, identifiers and destination, the new value and today’s consent. If the customer withdrew consent, don’t resend. According to the documentation, Google overwrites the value and keeps the conversion count. Check this after the first change.

Cancellation. Send it through the Google Ads API, method UploadConversionAdjustments, type RETRACTION, without a value. Google’s notice about closing parts of the Google Ads API names only conversion uploads. Cancellations went through the Google Ads API at the time of writing, and your first cancellation confirms it for your project.

Never send zero or a negative value. Google’s help on conversion adjustments says a value of 0 withdraws the conversion for good, and a retraction cannot be undone. What the Data Manager API does with a zero is not documented, so never send zero or a negative value. If an order’s margin falls to zero or below, send a cancellation.

Timing. Send a cancellation at least 24 hours after the conversion and within 54 days. According to Google’s help, a correction that arrives more than 7 days after the conversion is recorded is not used for bidding, so send corrections daily.

Limits. Adjustments in the Google Ads API don’t work with wbraid, so you can’t withdraw the conversion of an order sent with wbraid only. The import marks such a cancellation as unresolved, with the reason wbraid. Google’s documentation for the Data Manager API lists no such limit for margin changes, but check the result of the first margin change in the account. Never send an order to the Data Manager API again once a cancellation has been attempted for it, because Google could record it as a new conversion that you could not cancel a second time.

Step 8: Check that it works, then decide on bidding

In the first week, check four things every day:

  1. The margin action shows a value in “All conv. value (by conv. time)”, not only a count. Filter or segment the report by conversion action.
  2. Every request finishes with SUCCESS. Read the warnings too.
  3. The margin action’s “All conv. (by conv. time)” roughly matches the number of orders you have sent for the same days. A clear difference is a reason to look into it, not proof that the import sent an order twice.
  4. Google accepts the first cancellation row. The absence of an access error is not enough.

Then compare the margin action with the website tag’s action by conversion date, using only data for days that are at least 3 days old. The two actions can report different counts. Measure the difference in your account.

Bidding on margin is a separate decision, for when the values, cancellations and requests are all in order. Then make the margin action primary and, at the same moment, make the revenue action secondary and remove it from custom goals. Otherwise every purchase counts twice. Also check that each campaign uses the goal that contains the margin action. Recalculate the tROAS target too, because margin is lower than revenue.

Two more things to send with each order

New or returning customer. Fill userProperties.customerType (Step 6) from the shop’s order history. An order is NEW when there is no earlier non-cancelled order with the same email address or customer ID in the last 540 days, the window Google recommends. The order export must reach back that far. If you can’t tell, don’t send the field. Google uses this field for its new customer acquisition goal. Once the campaigns’ bidding uses the margin action (Step 8), open a Search, Performance Max, Shopping or Demand Gen campaign and click Campaign settings. Expand Customer acquisition and select Adjust your bidding to help acquire new customers.

Check. In the Campaigns table, click Segment and add New vs. returning customers. The split should roughly match what you sent. The report starts on the day you turn the goal on.

Your own variables. A custom variable adds a piece of information about the order to each conversion, for example B2B or B2C. First, in Google Ads, click Goals, then Conversions, then Custom variable and the plus button. Enter a lowercase name and click Save. Its status should read Enabled. The import sends its name and value in the event’s customVariables. Google ignores a variable that doesn’t exist in the account and shows no error. Never use an email address, phone number or name as its value. Google documents custom variables for reports, not for bidding.

Check. In the Campaigns table, click Segment, select Conversions, then Custom variable, and pick your variable. Each value should get its own row.

Take it with you

Download the instructions package for Claude Code

The package contains the whole procedure as a brief for Claude Code or a developer, with the setup checks, the click-matching query, the event structure and the daily corrections. It is not a finished program. Leave your email and the file is yours.

One file, one list. I only write when there's something worth reading.

FAQ

Does my shop have to store the Google Click ID?

No. The GA4 export to BigQuery stores the click with each visit that came from an ad, and the import finds it there through the order number. From the shop you only need the orders with purchase prices.

Do I need a developer token?

No. Google discontinued developer tokens on September 9, 2026. The Data Manager API does not use one, and access to the Google Ads API now belongs to a Google Cloud project.

Will the import bring view-through conversions?

Don’t count on them. The import’s conversion action is click-based. For video, Google documents only engaged-view conversions, which have to be uploaded within 14 days, and a daily run meets that.

What to do first

  1. Create the Cloud project and turn on the GA4 export to BigQuery today. The export has no history, so turn it on as early as possible.
  2. In Tag Assistant, check that your cookie banner grants ad_storage and ad_user_data together.
  3. Calculate the margin of five orders by hand. This shows whether your shop has a purchase price for every item.
The point of all this

Want this level of visibility in your account?

One e-mail. I'll tell you honestly whether it's worth it for your setup.

Get in touch →
Free download

Download the instructions package for Claude Code

The package contains the whole procedure as a brief for Claude Code or a developer, with the setup checks, the click-matching query, the event structure and the daily corrections. It is not a finished program. Leave your email and the file is yours.

One file, one list. I only write when there's something worth reading.