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.

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
timeStampnumberYesUnix timestamp (seconds) of when the conversion occurred
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)
utmSourcestringNoUTM source parameter (e.g. google) — stored for attribution reporting
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

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 the 24-hour Redis dedup rejected as duplicates of a recent submission. 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.

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 in the future (e.g. an appointment scheduled for next week), the system defers delivery instead of rejecting the conversion. A delayed job is enqueued and fires automatically after the timestamp passes — Google Ads only accepts past-dated conversion times, so deferral is the only path that lands the data.

Timestamp classBehavior
Past (or within clock-skew of now)Normal upload — uses the immediate queue
Future, 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"
}
}

Either way, poll GET /api/conversions/submissions/:id until status transitions away from pending_delivery to confirm Google Ads accepted the conversion.

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"
}

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
statusstringOne of the Status Values below

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.
completeAll conversions processed successfully
complete_with_errorsProcessing finished but some conversions failed — use Inspecting Failed Conversions to fetch the actual error messages
stalledProcessing has not progressed in 15 minutes — contact support
  1. Wait 30-60 seconds after submission before first poll.
  2. Continue polling until the response carries a terminal status — complete or complete_with_errors. pending and pending_delivery are both non-terminal and must not end polling.
  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. stalled means processing hasn't progressed in 15 minutes — stop polling and contact support.

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",
"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 !== 'pending'
3. If status === 'complete_with_errors':
GET /api/conversions/google/failed?submissionId=:id → per-row error details

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

The conversion timestamp is more than 30 days in the future (beyond MAX_DEFERRAL_DAYS). Timestamps within 30 days of the future are deferred automatically — see Deferred Delivery.

Fix: Verify the timestamp units (seconds, not milliseconds). The most common cause is milliseconds-as-seconds, which lands the timestamp ~50 years in the future.

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

The system automatically adjusts conversion timestamps to match your Google Ads account timezone. If your conversion timestamp doesn't align with the account's timezone (e.g. you send UTC but the account is in Eastern Time), the system will retry with timezone offsets to find a match.

You don't need to do anything — timezone conversion is automatic. Just ensure your timestamps are accurate relative to when the conversion actually occurred.

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 possiblegclid 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