Google Ads conversion tracking with AgentFirstStack

To track Google Ads conversions with AgentFirstStack, an agent adds one container snippet to your site and runs setup with your conversion ID and labels. The browser tag then reports each conversion with enhanced conversions data, and, if you add server credentials, the service also uploads click conversions through Google's Data Manager API with the stored gclid and hashed email or phone. Test traffic is never imported into Google Ads, and a test call returns Google's own answer.

Updated

The prompt

Paste this into your coding agent. It finishes everything it can, then asks only for the Google Ads values it couldn't find.

Paste this into Claude Code, Cursor or Codex

Set up Google Ads conversion tracking for this project with AgentFirstStack Tags. It's a free, open-source tag manager with server-side conversion APIs, built for agents, and you can do the setup yourself: there is no signup form. 1. Read https://agentfirststack.com/llms.txt and https://tags.agentfirststack.com/llms.txt. 2. If this project's .env already has AGENTFIRSTSTACK_API_KEY, use it and read AGENTFIRSTSTACK.md. Otherwise get a key: curl -X POST https://agentfirststack.com/v1/signup, and save api_key in .env as AGENTFIRSTSTACK_API_KEY and in the production secrets. 3. Create a tags container for the production domain, put its snippet in the head of every page, and serve it through a first-party proxy on our own domain (the configs from GET /v1/containers/:id/install?proxy=1, with the proxy key kept server-side), so ad blockers don't drop the conversion hits. Deploy, and run scan. If the site already has Google Tag Manager or a hard-coded AW- tag, tell me what it fires before you change anything. 4. Find the conversions that matter here (purchases, sign-ups, leads) and run setup with platforms.google_ads: the AW- conversion ID and a label per conversion for the browser tag, and, if I have them, server credentials for an Import from clicks conversion action. Map the visitor's email or phone where the page has it, for enhanced conversions. If you have Google Ads tools, use them to find the IDs; otherwise ask me for exactly what setup lists as missing. 5. Validate the draft and fix every error. If it warns google_ads_double_count, tell me which conversion action to keep as primary. Test the google_ads destination and show me Google's answer, open a preview on the conversion pages and read its log, then publish. 6. If the backend knows about conversions the browser doesn't (paid orders), send them to POST /v1/containers/:id/events with the visitor_id and event_id from the page. 7. Write the container id and the env var name (never the key) in AGENTFIRSTSTACK.md, and create a claim link for me with POST https://agentfirststack.com/v1/claim-link. Tell me what you set up, what each test returned, what's waiting on me, and give me the claim link.

Two ways Google Ads hears about a conversion

A conversion can reach Google Ads from the browser, from the server, or both. They use different kinds of conversion action in Google Ads, so decide which one counts.

Browser tagServer upload
Conversion action in Google AdsA website action, with a tag: AW- conversion ID and a labelAn "Import from clicks" action (type UPLOAD_CLICKS)
What it sendsgtag('event', 'conversion', {send_to, value, currency, transaction_id}), with hashed user_dataThe visitor's gclid, gbraid or wbraid, value, currency, transaction ID and hashed email, phone or address
From youThe conversion ID and labels (IDs only)customer_id, conversion_action_id, and an OAuth client id, secret and refresh token
CoversConversions that happen on a page, when the browser loads Google's tagConversions only your backend knows, even days after the click; and page conversions whose Google tag was blocked, if the hit still reached the service (see below)

If one conversion goes through both, Google Ads records it once per action. draft/validate warns google_ads_double_count in that case: keep only one of the two actions as a primary conversion in Google Ads, or drop one side. Many sites start with the tag and add the upload for purchases the backend confirms.

About ad blockers. Conversions your backend sends with POST /v1/containers/:id/events go from your server to ours to Google, so a browser can't block them. Browser hits are different: the default snippet loads from tags.agentfirststack.com and posts to it, and ad blockers can block that host. Serve the container through a path on your own domain with the proxy configs from GET /v1/containers/:id/install?proxy=1 (a Cloudflare Worker, Next.js, Vercel, Netlify or nginx; each sends the container's x-afs-proxy-key). There's no custom-domain (CNAME) option yet.

What the owner provides

AgentFirstStack never creates ad accounts or conversion actions; it sends to yours. An agent with Google Ads tools can look these up. Otherwise setup lists what's missing and where to find it:

  • Conversion ID and labels (browser): Google Ads > Goals > Conversions > the action > Tag setup > Install the tag yourself. The ID is AW-...; the label is the part after the slash in send_to.
  • Server upload: an Import from clicks conversion action and its numeric id (ctId= in the action's URL), the account's customer_id, and an OAuth client from the Google Cloud console with a refresh token for the scope https://www.googleapis.com/auth/datamanager. Add login_customer_id when access goes through a manager account.

Credentials are sealed at rest and never returned; responses show "configured": true.

Set it up by API

T=https://tags.agentfirststack.com
H="Authorization: Bearer $AGENTFIRSTSTACK_API_KEY"
J="content-type: application/json"
curl -s -X POST $T/v1/containers -H "$H" -H "$J" -d '{"domain":"example.com"}'
curl -s "$T/v1/containers/$C/install?proxy=1" -H "$H"   # first-party proxy configs (keep the key server-side)
# add the proxy and the proxied <script> to <head>, deploy, then:
curl -s -X POST $T/v1/containers/$C/scan -H "$H"
curl -s -X POST $T/v1/containers/$C/setup -H "$H" -H "$J" -d '{
  "platforms": {
    "google_ads": {
      "conversion_id": "AW-123456789",
      "labels": { "purchase": "AbCdEfGhIj" },
      "server": {
        "customer_id": "123-456-7890", "conversion_action_id": "987654321",
        "client_id": "...apps.googleusercontent.com", "client_secret": "...", "refresh_token": "1//..."
      }
    }
  },
  "conversions": [{
    "name": "purchase", "when": { "data_layer_event": "purchase" },
    "value": { "data_layer": "ecommerce.value" }, "currency": "USD",
    "transaction_id": { "data_layer": "ecommerce.transaction_id" },
    "email": { "data_layer": "customer.email" }
  }]
}'
# -> draft, validation, destinations, missing[] (what's still needed, and where)
curl -s -X POST $T/v1/containers/$C/draft/validate -H "$H"
curl -s -X POST $T/v1/containers/$C/destinations/$D/test -H "$H"    # Google's answer, validateOnly
curl -s -X POST $T/v1/containers/$C/preview -H "$H"                  # preview_url on the live site
curl -s -X POST $T/v1/containers/$C/publish -H "$H" -H "$J" -d '{"note":"google ads purchase"}'

Leave out server to start with the browser tag only, and run setup again when the credentials arrive: it merges into the draft. Conversions can also fire on a thank-you page (thank_you_path), a form submit (form_selector) or a click (click_selector). Over MCP the same calls are tags_setup_conversion_tracking, tags_validate_draft, tags_test_destination and tags_publish.

Enhanced conversions

Enhanced conversions send hashed customer data with a conversion. Google's help describes the feature as sending "hashed first-party, user-provided data from your website when a user converts", hashed with SHA-256 (About enhanced conversions, as of 6 Nov 2026).

  • In the browser, the Google Ads tag is configured with allow_enhanced_conversions: true. When a conversion has an email or phone mapped (or user_data: "auto", which takes the submitted form's first email and phone field), the runtime hashes them with SHA-256 in the browser and calls gtag('set', 'user_data', ...) with sha256_email_address, sha256_phone_number and, when known, hashed first and last name with the postal code and country. That happens only when the visitor's ad_user_data consent is granted.
  • On the server, each upload carries the same hashed email and phone as user identifiers next to the click id (through the Data Manager API, also an address when first name, last name, country and postal code are all known). Google's Data Manager guide covers this as "sending Google Ads offline conversions or enhanced conversions for leads" (Send offline conversions, as of 6 Nov 2026).
  • In Google Ads, you turn it on: Goals > Settings, expand "Enhanced conversions", check "Turn on enhanced conversions", agree to the customer data terms and choose "Google tag" as the method (Set up enhanced conversions for web using the Google tag, as of 6 Nov 2026). This is a person's decision in their account, so the agent lists it as waiting on you.

Conversions from your backend

For a purchase the browser can't see (a paid invoice, an order confirmed by a webhook), the backend reports it and the service joins it to the visitor's stored click id. Click ids from the landing URL are kept for 90 days.

curl -s -X POST $T/v1/containers/$C/events -H "$H" -H "$J" -d '{"events":[{
  "name": "purchase", "visitor_id": "'$VISITOR_ID'", "event_id": "'$EVENT_ID'",
  "order_id": "T1001", "value": 49, "currency": "USD", "email": "jane@example.com"
}]}'
  • visitor_id comes from afst('get','visitor_id') on the page, passed along with the checkout request. Pass the browser conversion's afst('get','event_id') as event_id if the page also tracked it.
  • The upload uses your order_id as the transaction ID, or the event_id when there's none. Google Ads uses the transaction ID to catch duplicates for the same conversion action (Use a transaction ID to minimize duplicate conversions, as of 6 Nov 2026); in the Data Manager API, an event whose transaction ID already exists is handled as an adjustment (upgrade guide, as of 6 Nov 2026).
  • Emails and phones are normalised and hashed before anything is sent. With no click id and no hashed email or phone, the delivery is skipped and the event log says why. Without ad_user_data consent it's skipped too.

Test traffic never becomes a conversion

An agent checking your setup clicks through the site like a visitor. None of that may count in Google Ads:

  • In the browser: a tab opened with ?afsa_debug=1, through a preview link, or after afst('test', true) is test traffic. The Google Ads tag doesn't fire there; the preview log shows it as test_skipped ("ad pixels don't fire in the browser; the server-side conversion is sent in test mode").
  • On the server: test conversions, backend events sent with "test": true and destination tests go to Google with validateOnly: true. Google's guide says this checks the request "without ingesting any data" (Data Manager guide, as of 6 Nov 2026). You get Google's real validation errors, and nothing is imported.

Verify it

  1. Destination test: POST /v1/containers/:id/destinations/:did/test returns { result: { ok, http_status, response }, test_mode }, where response is Google's answer. A bad refresh token or a conversion action of the wrong type shows up here.
  2. Preview: POST /v1/containers/:id/preview returns a preview_url that runs the draft on the live site. Its log (GET .../preview/:token/log, MCP tags_get_preview_log) lists each trigger matched and each tag fired or skipped, with the reason.
  3. The whole setup: POST https://agentfirststack.com/v1/setup/test sends each conversion to each ad platform in its test mode and returns a browser plan; GET /v1/setup then reports the tags rows, ad platform deliveries included.
  4. After real traffic: GET /v1/containers/:id/events?since=1h shows each conversion with its status per destination, attempts, Google's HTTP status and any error. Transient failures (network errors, 429, 5xx) are retried with backoff, up to 6 attempts.

Data Manager API or Google Ads API

The google_ads destination uploads through Google's Data Manager API by default (POST https://datamanager.googleapis.com/v1/events:ingest). Google's upgrade guide lists the developer token as "Not required" there and "Required" for the Google Ads API (upgrade guide, as of 6 Nov 2026). If you already upload with a Google Ads API developer token, set "api": "google_ads_api" with developer_token and a refresh token for the adwords scope; the server-side guide has the background.

FAQ

Does AgentFirstStack support Google Ads enhanced conversions?

Yes, on both sides. The browser tag loads with allow_enhanced_conversions and sets user_data with SHA-256 hashed email, phone and address fields. The server upload sends hashed email, phone and address as user identifiers next to the gclid. You turn enhanced conversions on in Google Ads and accept its customer data terms.

Will a conversion count twice if I use the tag and the upload?

It can. The tag reports to a website conversion action and the upload to an Import from clicks action, and Google Ads counts each action separately. The validator warns (google_ads_double_count) when one conversion does both: keep one action as primary in Google Ads, or drop one side.

Do I need Google Tag Manager or a developer token?

No. The container replaces the web container, and server uploads go through Google's Data Manager API, which needs no developer token. The older Google Ads API path is available with api set to google_ads_api, and that one needs a developer token.

Can test traffic end up as a conversion in Google Ads?

No. In test traffic (a preview link, ?afsa_debug=1 or afst('test', true)) the Google Ads tag doesn't fire in the browser, and server uploads are sent with validateOnly, which Google validates without ingesting any data. Backend events sent with "test": true are handled the same way.

What if a visitor has no gclid?

The upload still goes out if the conversion carries a hashed email or phone. With no click id (gclid, gbraid or wbraid) and no hashed email or phone, the delivery is logged as skipped with that reason.

Product names belong to their owners. AgentFirstStack isn't affiliated with Google.