Skip to main content

Google Ads Conversions

This guide covers how to submit offline conversions to Google Ads through the TNT Growth API, check submission status, and investigate per-conversion results.

See duplicate submissions and resends for how Google /add handles an existing conversion identity, including resends after 24 hours.

Base URL​

All endpoints are mounted under the /api prefix on the Flex host:

https://flex.tntgrowth.io/api/...

Authentication​

All requests require an x-api-key header with your client-specific API key provided by TNT Growth.

-H "x-api-key: YOUR_API_KEY"

The API key is scoped to a single client (= one Google Ads customer ID). All submission and lookup endpoints enforce that the customer_id in the payload matches the Google Ads customer ID linked to your API key. A mismatch returns 403 Forbidden.

Submitting Conversions​

Endpoint​

POST /api/google/conversions/add

Request​

Send an array of conversion objects. Each request accepts a maximum of 100 conversions. For larger batches, chunk your data into groups of 100 and submit multiple requests. Exceeding 100 conversions returns a 413 Payload Too Large error.

Request Body Schema​

FieldTypeRequiredDescription
customerIdstringYesGoogle Ads customer ID (no dashes)
conversionActionIdstringYesThe conversion action ID from Google Ads
timeStampstringYesUnix timestamp in seconds, as a string (e.g. "1778043599")
gclidstringNo*The Google Click ID captured from the ad click
gbraidstringNo*Google Brand Retail Ads ID (iOS privacy-safe alternative to gclid)
wbraidstringNo*Web Retail Ads ID (web privacy-safe alternative to gclid)
firstTouchUrlstringNoLanding page URL — gbraid/wbraid are auto-extracted from query params if not provided directly
conversionValuenumberNoMonetary value of the conversion. Defaults to 0 if omitted
emailstringNoUser email for enhanced conversions (see Enhanced Conversions)
phonestringNoUser phone for enhanced conversions (see Enhanced Conversions)
tntIdstringNoThe attribution handle from the pixel. A resolved session fills gclid and any missing UTM fields
utmSourcestringNoUTM source parameter (e.g. google) — stored for attribution reporting, and also decides delivery when it names a different paid platform. See When we withhold a conversion
utmMediumstringNoUTM medium parameter (e.g. cpc) — stored for attribution reporting
utmCampaignstringNoUTM campaign name — stored for attribution reporting
utmTermstringNoUTM search keyword — stored for attribution reporting
utmContentstringNoUTM content parameter (ad/creative variant) — stored for attribution reporting

* Click identifier requirement: You must provide at least one of gclid, gbraid, or wbraid — OR — provide email and/or phone for enhanced conversion matching. If all identifiers are omitted, the conversion will fail with MISSING_IDENTIFIER_ERROR.

Priority: When multiple click identifiers are provided, the system uses gclid first, then gbraid, then wbraid.

Example Request​

Pick your language — the choice syncs across every example on the site.

curl -X POST https://flex.tntgrowth.io/api/google/conversions/add \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '[
{
"customerId": "4482546575",
"conversionActionId": "7567171084",
"gclid": "Cj0KCQjwh-HPBhCIARIsAC0p3ce...",
"timeStamp": "1778043599",
"conversionValue": 1,
"utmSource": "google",
"utmMedium": "cpc",
"utmCampaign": "brand-search-q2",
"utmTerm": "best treatment center",
"utmContent": "headline-variant-a"
},
{
"customerId": "4482546575",
"conversionActionId": "7567171084",
"gclid": "EAIaIQobChMI4Yb...",
"timeStamp": "1778043610",
"conversionValue": 50,
"email": "user@example.com",
"phone": "+14155552671",
"utmSource": "google",
"utmMedium": "cpc",
"utmCampaign": "competitor-keywords"
}
]'

Example Response​

{
"message": "Conversions queued successfully",
"submissionIds": [
"8f3c2d10-7e6b-4a51-9d8c-3b2e1f0a9b8c"
],
"statusUrls": [
"/api/conversions/submissions/8f3c2d10-7e6b-4a51-9d8c-3b2e1f0a9b8c"
],
"batchStatusUrl": "/api/google/conversions/status"
}
FieldDescription
submissionIdsOne UUID per (customerId, clientId) group in the batch — poll each one independently
statusUrlsConvenience — pre-built URLs for the submission status endpoint
batchStatusUrlThe shared batch-lookup URL for per-conversion status
deduped(Optional) Count of conversions Flex deduplicated against an in-progress or delivered conversion. Not an error — surfaced so callers can distinguish "Google accepted this" from "silently skipped because we already sent this payload".

Response Codes​

StatusWhen
200All conversions queued (or successfully deferred — see Deferred Delivery)
207Multi-status — submission accepted, but one or more conversions hit a pre-Google validation error. Inspect data for the per-row errors, or follow the two-call pattern in Inspecting Failed Conversions In Your Batch.
413More than 100 conversions in one request — chunk and retry
500Internal error — submissionIds still surfaces any partial commits

Enhanced Conversions (Email & Phone)​

Enhanced conversions allow Google Ads to match conversions even when a click identifier (gclid/gbraid/wbraid) is missing or belongs to a different account. Useful for:

  • Conversions where the GCLID was lost or not captured
  • Cross-account click matching (GCLID from a different MCC hierarchy)
  • Improving match rates alongside click identifiers

How Hashing Works​

Google Ads requires user identifiers to be SHA-256 hashed before upload. You have two options:

Send plain email and phone — the system normalizes and hashes them:

  • Email: lowercased and trimmed before hashing
  • Phone: formatted to E.164 international standard (e.g. +14155552671) before hashing
{
"email": "User@Example.com",
"phone": "415-555-2671"
}

Option 2: Send pre-hashed values​

If you already have SHA-256 hashes, send them directly. The system auto-detects hashed values (64-character hex strings) and uses them as-is:

{
"email": "a1b2c3d4e5f6...64_char_sha256_hash",
"phone": "f6e5d4c3b2a1...64_char_sha256_hash"
}
tip

When in doubt, send raw values and let the system handle normalization and hashing. This avoids common issues with incorrect hashing (e.g. hashing before lowercasing the email).

Requirements​

Enhanced conversions must be enabled in your Google Ads account:

  1. Go to Google Ads > Goals > Conversions > Settings
  2. Enable Enhanced conversions for leads

If not enabled, conversions with only email/phone identifiers will fail with an "enhanced conversions not enabled" error.

Batching​

The API accepts an array of conversion objects, up to 100 per request.

One batch, one visitor's tntId

Entries are grouped by customerId into a single submission, and a submission records one attribution handle. So within a group:

  • All entries carry the same tntId (or none at all) — that handle attributes the group. Normal case.
  • Some carry it, some do not — the one handle present is still applied to the whole group. Do not batch several visitors together and set tntId on only one of them.
  • Entries carry different tntId values — the group is left entirely unattributed. We do not guess which visitor it belongs to, and the fallback that would have matched a session on the gclid is skipped as well.

If you are submitting a day's conversions for many visitors, that last case is the one you will hit. Either send one request per visitor, or leave tntId off and let the click IDs do the matching. Each entry's own tntId is still stored with it either way, so nothing is lost for later investigation.

That is why the batch examples above carry no tntId — they are two visitors under one customerId. One visitor's conversion, with the handle, looks like this:

curl -X POST https://flex.tntgrowth.io/api/google/conversions/add \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '[
{
"customerId": "4482546575",
"conversionActionId": "7567171084",
"gclid": "Cj0KCQjwh-HPBhCIARIsAC0p3ce...",
"timeStamp": "1778043599",
"conversionValue": 1,
"tntId": "9f2b7c1e-4d3a-4f8b-9c2e-1a5d7e0b4c63"
}
]'
ScenarioRecommendation
1-100 conversionsSend in a single request
101-1000 conversionsChunk into groups of 100, send sequentially
1000+ conversionsChunk into groups of 100 with a 1-2 second delay between requests
const chunkSize = 100;
for (let i = 0; i < conversions.length; i += chunkSize) {
const batch = conversions.slice(i, i + chunkSize);
await fetch('/api/google/conversions/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': API_KEY },
body: JSON.stringify(batch),
});
}

Deferred Delivery (Future Timestamps)​

When a conversion's timeStamp is beyond the five-minute clock-skew tolerance but within 30 days (e.g. an appointment scheduled for next week), supported ingestion paths defer delivery. A delayed job fires after the timestamp passes. Near-future clock skew uses the immediate path with a timestamp correction; it is not scheduled as a future appointment.

Timestamp classBehavior
PastNormal upload — uses the immediate queue
Up to five minutes aheadImmediate upload with the timestamp clamped to upload time
More than five minutes ahead, within 30 daysDeferred — scheduled to fire after timeStamp
Future, more than 30 days outRejected as BAD_TIMESTAMP_OR_TOO_FAR_FUTURE

The 30-day ceiling (MAX_DEFERRAL_DAYS) protects against units-confusion bugs (milliseconds-as-seconds typically lands the timestamp years in the future).

How the response surfaces deferral​

  • POST /api/google/conversions/add — returns 200 as usual; the submission row carries scheduledFor and its status becomes pending_delivery (see Status Values) until the deferred job fires.
  • TNT-integrated webhook ingest paths (Calendly, CallTracking Metrics) — return 202 Accepted with a submissionId you can poll. The 202 is a signal that the conversion was accepted but is not yet on its way to Google Ads.
HTTP/1.1 202 Accepted
{
"message": "Deferred CTM conversion for Acme until 2026-06-04T15:30:00.000Z (submission 8f3c2d10-7e6b-4a51-9d8c-3b2e1f0a9b8c)",
"data": {
"submissionId": "8f3c2d10-7e6b-4a51-9d8c-3b2e1f0a9b8c"
}
}

Poll GET /api/conversions/submissions/:id and inspect the resulting counts and status. Leaving pending_delivery alone does not establish success: the attempt can fail or be withheld.

Checking Submission Status​

After submitting conversions, poll the submission status to determine when processing is complete.

Endpoint​

GET /api/conversions/submissions/:submissionId

Path Parameters​

ParameterTypeDescription
submissionIdstringThe UUID returned by the submit response

Example Request​

curl https://flex.tntgrowth.io/api/conversions/submissions/8f3c2d10-7e6b-4a51-9d8c-3b2e1f0a9b8c \
-H "x-api-key: YOUR_API_KEY"

Example Response​

{
"id": "8f3c2d10-7e6b-4a51-9d8c-3b2e1f0a9b8c",
"source": "GOOGLE_ADS",
"externalAccountId": "4482546575",
"expectedCount": 2,
"createdAt": "2026-05-11T12:00:00.000Z",
"scheduledFor": null,
"counts": {
"success": 2,
"failed": 0
},
"status": "complete"
}

Example Response — a withheld conversion​

Both conversions below were processed, but one carried utmSource: "facebook" with no gclid, so it was withheld from Google rather than uploaded. counts.success is 1, not 2, and counts.failed stays 0 because nothing was rejected — the status is complete because every pair reached a final outcome.

{
"id": "4b1a77e2-0c93-4f28-9a6d-51ec7d3b2f40",
"source": "GOOGLE_ADS",
"externalAccountId": "4482546575",
"expectedCount": 2,
"createdAt": "2026-05-11T12:00:00.000Z",
"scheduledFor": null,
"counts": {
"success": 1,
"failed": 0,
"withheld": 1
},
"withheld": [
{
"reason": "cross_channel",
"claimedPlatform": "meta",
"count": 1
}
],
"status": "complete"
}

Response Fields​

FieldTypeDescription
idstring (UUID)Submission ID
sourcestringConversion platform — GOOGLE_ADS for this endpoint
externalAccountIdstringGoogle Ads customer ID this submission was scoped to
expectedCountnumberNumber of conversion rows in the submission
createdAtstring (ISO-8601)When the submission was created
scheduledForstring (ISO-8601) | nullPresent and non-null when the submission was deferred for future delivery — the ISO timestamp at which the queued job will fire (see Deferred Delivery)
counts.successnumberConversions Google Ads accepted
counts.failednumberConversions that failed pre- or post-upload
counts.dedupednumber (optional)Conversions in this submission Flex deduplicated against an in-progress or delivered conversion. Present only when non-zero. These were not enqueued again. This does not confirm the platform received them — see deduped
counts.withheldnumber (optional)Conversions deliberately not sent to Google because they carried no Google click ID and either their utmSource named a different paid platform or your account sends Google only click-attributed conversions. Present only when non-zero, and already excluded from counts.success. Not a failure — see Withheld Conversions
withheldarray (optional)One entry per reason, and for cross_channel per claiming platform, present only when counts.withheld is. See Withheld Conversions
statusstringOne of the Status Values below

Withheld Conversions​

A conversion is withheld when its attribution says another paid channel served the click — utmSource: "facebook" with no gclid, for example. Uploading it would train Google's bidding on traffic Google never sent, so we hold it back. When TNT Growth has set your account to send Google only click-attributed conversions, a conversion with no gclid, gbraid or wbraid is withheld as well, including an email- or phone-only enhanced match. The full rules, and the utm_source values that count as a claim, are in When we withhold a conversion from a platform.

This is not an error. A withheld pair is counted in counts.withheld, excluded from counts.success, and never appears in counts.failed — failed means Google rejected it. A submission whose only non-delivered pairs were withheld finishes as complete, so a complete status does not on its own mean everything was delivered. Check counts.success against expectedCount if you need that.

Each entry in the withheld array describes one reason, and for cross_channel one claiming platform:

FieldTypeDescription
reasonstringcross_channel, click_id_required (the click-ID rule above), or other for a reason not documented yet. Treat any unrecognised value the same way rather than failing
claimedPlatformstring (optional)On cross_channel entries only: the channel the utmSource claimed, google, meta, bing, tiktok, linkedin, reddit or openai
countnumberConversions in this submission withheld for that reason. The entries sum to counts.withheld

Nothing is discarded — we keep the full payload, so a withheld conversion can still be delivered later if the rules or your tagging change. If you believe a conversion was withheld wrongly, the usual cause is utmSource carrying last-touch tagging from another channel; send the Google click ID (gclid, gbraid or wbraid) alongside it and the click ID wins.

Status Values​

StatusDescription
pendingConversions are queued and waiting to be processed
pending_deliveryThe submission has a future-dated timeStamp. The system has scheduled it for delivery and will send to Google Ads automatically after the timestamp passes. The scheduledFor field carries the ISO-8601 timestamp at which the conversion is queued to fire.
completeEvery conversion reached a final outcome and none was rejected. Usually means all were delivered — but a withheld conversion also finishes here, so compare counts.success with expectedCount if you need delivery confirmed
complete_with_errorsProcessing finished but some conversions failed — use Inspecting Failed Conversions to fetch the actual error messages
dedupedEvery conversion in this submission was deduplicated against an in-progress or delivered conversion, so this submission was not enqueued again. Not an error, and not necessarily final — it is reported only while this submission has zero results, and the job that owns the payload may still complete under this submission, at which point the status becomes complete or complete_with_errors. It does not by itself confirm delivery
stalledProcessing has not progressed in 15 minutes — contact support
  1. Wait 30-60 seconds after submission before first poll.
  2. Stop polling once the response carries complete, complete_with_errors or stalled. The first two describe completed outcomes at the time of the response; late confirmed recovery can revise a previously failed claim. stalled is not an outcome but a signal to give up and escalate (step 8). pending and pending_delivery still need a delivery result. For deduped, use the bounded recheck in step 7; it does not establish delivery.
  3. For pending, poll every 5-10 seconds.
  4. For pending_delivery, the scheduledFor timestamp tells you when delivery is queued to fire — there is no value in polling faster than that. Resume the 5-10 second cadence once scheduledFor has passed, since the deferred worker transitions the submission to complete / complete_with_errors shortly after.
  5. When the status becomes complete_with_errors, call the failed-conversions endpoint with ?submissionId=... to fetch the actual errors.
  6. On a terminal response, read counts.withheld before treating counts.success as the whole story. A non-zero value is not a failure and will not change on a retry — the withheld array names the claiming platform so you can fix the tagging at source. Re-sending the same payload will be withheld again.
  7. deduped means this request did not enqueue another delivery. It is terminal for submission bookkeeping and may remain unchanged indefinitely, although a racing worker can attach a result and change it to complete. Recheck only within a client-side polling deadline. If it remains deduped, inspect the original submission to confirm delivery; do not wait for this duplicate to become stalled.
  8. stalled means processing hasn't progressed in 15 minutes — stop polling and contact support.

Late delivery results​

A failed Google claim is not irreversible: a later confirmed upload can settle it as succeeded. A stored failed-attempt row can therefore coexist with a later successful conversion result. Check the current result before resubmitting. When a recovery is being investigated, recheck the existing submission rather than assuming its earlier error remains current.

For Data Manager delivery, acceptance is asynchronous and does not by itself confirm delivery. Flex keeps the claim pending for matching diagnostics; another generic success update cannot clear that hold. Platform acceptance also does not establish attribution credit.

Inspecting Failed Conversions In Your Batch​

When a submission ends with complete_with_errors, the submission status endpoint surfaces only the count of failed rows. To fetch the actual error messages for the rows that failed, use the per-platform failed-conversions endpoint with ?submissionId=<id>.

Endpoint​

GET /api/conversions/google/failed?submissionId=<submissionId>

Query Parameters​

ParameterTypeDescription
submissionIdstring (UUID)Filter to a single submission's failed rows
errorCategorystringOptional — filter by category label (see Error Categories)
limitnumberPage size (1-1000, default 50)
offsetnumberPage offset (default 0)

Example Request​

curl "https://flex.tntgrowth.io/api/conversions/google/failed?submissionId=8f3c2d10-7e6b-4a51-9d8c-3b2e1f0a9b8c" \
-H "x-api-key: YOUR_API_KEY"

Example Response​

{
"message": "Retrieved 1 failed google conversions",
"data": {
"data": [
{
"id": 81923,
"errorMessage": "The click occurred outside of the conversion action's click-through window (90 days)",
"errorCategory": "Click-through window expired",
"errorClass": "PERMANENT_DATA",
"retryable": false,
"clientId": "client_abc123",
"clientName": "Acme",
"platformId": "4482546575",
"createdAt": "2026-05-11T12:00:33.000Z",
"ingestionSource": "API",
"reprocessedAt": null,
"request": {
"customerId": "4482546575",
"conversionActionId": "7567171084",
"gclid": "Cj0KCQjwh-HPBhCIARIsAC0p3ce...",
"timeStamp": "1772044999",
"conversionValue": 1
}
}
],
"pagination": {
"total": 1,
"limit": 50,
"offset": 0
}
}
}
1. POST /api/google/conversions/add → { submissionIds: [id] }
2. GET /api/conversions/submissions/:id → poll until status is 'complete', 'complete_with_errors', or 'stalled'
3. If status === 'complete_with_errors':
GET /api/conversions/google/failed?submissionId=:id → per-row error details

Apply a client-side polling deadline to step 2. A persistently deduped submission requires the original submission's delivery result, as described in the polling pattern.

Error Classes and Retry Availability​

The shared response also includes nullable errorClass and boolean retryable. errorClass records the delivery policy classification; errorCategory below remains a human-readable message label. Legacy null classes are displayed as Not classified, distinct from the UNCLASSIFIED class. Use Delivery Errors and Retry Status for the complete class reference and platform limits. reprocessedAt marks reprocessing, not successful delivery.

Error Categories​

Each failed-conversion row is tagged with an errorCategory label so callers can group by failure class without parsing the raw errorMessage. The same label can be passed back as the errorCategory query parameter to filter the list.

Category labelTriggered by errorMessage containing
Click-through window expiredclick-through window
Timestamp precedes clickprecedes the click
Click not foundclick_not_found
Not authorizednot authorized
Action not foundconversion action not found
Enhanced conversions configenhanced conversions
Future timestampafter allowed maximum
OtherAnything that doesn't match the patterns above

Detailed Per-Conversion Status (Batch Lookup)​

For granular visibility into individual conversion results, use the batch status endpoint. This is the right call when you need to confirm whether a specific (customer_id, gclid, conversion_action_id) triple landed — for example, reconciling a CRM export against the data you submitted weeks earlier.

Endpoint​

POST /api/google/conversions/status

Request​

The request body is a JSON object with a single key conversions whose value is an array of lookup tuples. Each request accepts a maximum of 100 conversions.

Request body envelope

The status endpoint expects { "conversions": [...] } — not a bare array. Submitting a bare array returns 400 Bad Request at the schema-validation layer.

Field naming

The status endpoint uses snake_case field names (customer_id, conversion_action_id), while the submission endpoint uses camelCase (customerId, conversionActionId). This is intentional and not a typo.

Request Body Schema​

FieldTypeRequiredDescription
conversionsarrayYes1-100 lookup tuples
conversions[].customer_idstringYesGoogle Ads customer ID (no dashes) — must match the API key's client
conversions[].gclidstringYesThe Google Click ID submitted with the original conversion
conversions[].conversion_action_idstringYesThe conversion action ID submitted with the original conversion

Example Request​

curl -X POST https://flex.tntgrowth.io/api/google/conversions/status \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"conversions": [
{
"customer_id": "4482546575",
"gclid": "Cj0KCQjwh-HPBhCIARIsAC0p3ce...",
"conversion_action_id": "7567171084"
},
{
"customer_id": "4482546575",
"gclid": "EAIaIQobChMI4Yb...",
"conversion_action_id": "7567171084"
}
]
}'

Example Response​

{
"results": [
{
"customer_id": "4482546575",
"gclid": "Cj0KCQjwh-HPBhCIARIsAC0p3ce...",
"conversion_action_id": "7567171084",
"status": "processed",
"recorded_at": "2026-04-09T17:33:21.043Z"
},
{
"customer_id": "4482546575",
"gclid": "EAIaIQobChMI4Yb...",
"conversion_action_id": "7567171084",
"status": "partial_failure",
"message": "The click occurred outside of the conversion action's click-through window",
"recorded_at": "2026-04-09T17:33:21.043Z"
}
]
}

Per-Conversion Status Values​

StatusDescription
processedConversion uploaded successfully to Google Ads
pendingConversion has been claimed for upload but Google has not responded yet
partial_failureGoogle accepted the request but reported an error for this conversion
failed_pre_googleConversion failed validation before reaching Google Ads (e.g. click-through window pre-filter)
not_foundNo matching conversion record found in the system

Per-Result Fields​

FieldTypeNotes
customer_idstringEchoed from the request tuple
gclidstringEchoed from the request tuple
conversion_action_idstringEchoed from the request tuple
statusstringSee enum above
messagestring(Optional) Specific error detail when status is not processed. Omitted for processed and not_found.
recorded_atstring (ISO-8601)(Optional) When the conversion (or failure) was recorded

Response Codes​

StatusWhen
200Lookup completed (results reflects per-row status — including not_found)
400Bad request — most commonly a missing conversions envelope or a malformed tuple
401Missing or invalid x-api-key
403One or more customer_id values don't match the Google Ads customer linked to the API key, or the client has no Google Ads customer configured
500Internal error during lookup

Common Errors & Troubleshooting​

When a conversion fails with partial_failure, the message field contains the specific error from Google Ads. Here are the most common errors and how to resolve them.

Click-Through Window Expired​

Error: The click occurred outside of the conversion action's click-through window

The time between the ad click and the conversion exceeds the conversion action's configured window (typically 30-90 days).

Conversions older than the Google Ads conversion action's click_through_lookback_window_days are pre-filtered before upload — they are persisted as failed_pre_google rows with the conversion age and the configured window in the errorMessage, instead of going through the retry loop. This makes the failure visible immediately on /status and /failed without waiting for Google to reject.

Fix: Check your CRM data freshness. If conversions are consistently arriving late, extend the click-through window in Google Ads > Goals > Conversion Action > Settings.

Future Timestamp​

Error: Date is after allowed maximum

This is a Google upload rejection; it is not the same as Flex rejecting a timestamp beyond its 30-day deferral ceiling. A small clock discrepancy can also exceed Google's accepted maximum. Flex may perform the single correction described in Timezone Handling.

Fix: Verify timestamp units and the source clock, and inspect any timestamp-correction alert. For intentional future appointments, use a supported deferred ingestion path.

Click Not Found​

Error: The click ID could not be matched to an existing Google Ads click

The GCLID doesn't match any click in the Google Ads account.

Fix:

  • Verify the GCLID is correctly captured (no truncation or encoding issues)
  • Confirm the GCLID belongs to the correct Google Ads account
  • If the GCLID is from a different account, provide email/phone for enhanced conversion fallback

Conversion Action Not Found​

Error: Conversion action not found

The conversionActionId doesn't exist in the specified Google Ads account.

Fix: Verify the conversion action ID is correct and that the API has access to the account.

Not Authorized (Foreign GCLID)​

Error: Not authorized to access the customer account

The GCLID belongs to a Google Ads account outside your MCC hierarchy.

Fix: Provide email and/or phone so the system can use enhanced conversion matching instead of the GCLID.

Enhanced Conversions Not Enabled​

Error: Enhanced conversions settings are not configured

The Google Ads account doesn't have Enhanced Conversions for Leads enabled.

Fix: Enable it in Google Ads > Goals > Conversions > Settings > Enhanced conversions for leads.

Timestamp Precedes Click​

Error: The conversion timestamp precedes the click timestamp

The conversion is recorded as happening before the user clicked the ad.

Fix: Ensure you're using the actual conversion timestamp, not the click time or an incorrect date.

Timezone Handling​

Flex formats the supplied instant in the Google Ads account timezone. It does not search successive timezone offsets until Google accepts the conversion. Supply the actual event time.

A timestamp up to five minutes ahead is treated as clock skew and clamped to upload time before upload, with the original and adjusted values logged. Larger future timestamps follow the deferral rules on paths that support deferral.

If Google rejects a time as preceding the click or after its allowed maximum, the legacy upload path can make one corrective retry clamped to upload time. Success after that correction emits a best-effort data-quality alert: the platform received the adjusted time, not the original event time. The Data Manager path logs its one-time correction but does not currently raise this alert. Correct the upstream clock/timezone issue even if delivery succeeds; repeated timestamp changes are not a safe retry strategy.

Best Practices​

  • Chunk large batches into groups of 100 conversions per request
  • Store submission IDs from responses for status tracking
  • Poll with a delay — wait 30-60 seconds after submission before checking status, as conversions are processed asynchronously
  • Treat pending_delivery as expected for any conversion submitted with a future timestamp — scheduledFor tells you exactly when delivery is queued to fire
  • Use the two-call pattern for errors — when a submission reports complete_with_errors, call /api/conversions/google/failed?submissionId=... to get the actual error messages for the failed rows
  • Use batch status for spot-checks — /api/google/conversions/status is the right call when you have a list of (customer_id, gclid, conversion_action_id) triples and want to confirm whether each landed
  • Provide click identifiers when possible — gclid gives the best match rate, followed by gbraid/wbraid
  • Include email/phone as fallback — even when you have a GCLID, providing email or phone improves match rates and handles foreign-account GCLIDs
  • Send raw values for hashing — let the system normalize and hash email/phone rather than hashing yourself, to avoid common normalization mistakes
  • Validate GCLIDs — ensure they are not truncated, double-encoded, or from a different account
  • Check conversion action IDs — verify they exist in the target Google Ads account before submitting
  • Send tntId when your backend has it — the attribution handle recovers the click ID and UTMs captured on the visitor's original ad click, which is often the only thing that attributes an offline conversion at all