To attribute phone calls to Google Ads, pull each tracked call and its gclid (Google's click id) out of CallRail, match it to a real booking, and upload it as an offline conversion. To avoid double-counting, set just one conversion action as primary for calls: CallRail's native import or your own upload.
Without this loop, phone leads can stay invisible to Google: someone clicks an ad, calls, books days later, and the platform never learns the click worked.
I built patient conversion tracking for a multi-location US medical practice that ran Google Ads for calls and online bookings.
Quick answer
Build the loop in three moves, and settle one decision before you write any code.
| Step | What it does |
|---|---|
| Pull calls with gclid | Export each tracked call from CallRail, including the gclid it captured from the landing page |
| Match call to booking | Join on a SHA-256 hash of the phone number, inside a 1 to 2 day window |
| Upload as offline conversion | Send it through the Data Manager API to a dedicated UPLOAD_CLICKS conversion action |
The decision: if CallRail's native Google Ads integration already imports calls, do not add a second primary "qualified call" action. Google reports every primary action in its Conversions column, so each call lands twice. Pick one for bidding and keep the other as observation.
Why phone leads break Google Ads attribution
Standard Google Ads conversion tracking sees a form submit because the gclid is in the browser when the form fires. A phone call breaks that chain: the click id is nowhere near the conversation. Unless something carries the gclid across that gap, Google credits the click with nothing, and smart bidding favors the leads it can see (forms) over the channel it cannot (calls).
CallRail closes the gap. It swaps in a dynamic number per visitor, ties it to the session, and captures the gclid from the landing page URL, so each call knows its click. Your job is to decide which calls count, attach a value, and hand them back to Google.
How do I match a call to a booking?
A ringing phone is not a conversion. The caller may be an existing patient, a wrong number, or someone who never books. Credit the ad only when a call becomes a real appointment, so join the call log to the booking record.
| Signal | Rule | Why it matters |
|---|---|---|
| Phone number | SHA-256 hash matches between the call and the booking | Ties the two records without ever storing the raw number |
| Time window | Booking created within 1 to 2 days of the call | A wider window sweeps in organic bookings the call never caused |
| gclid present | The call carried a gclid from an ad click | No gclid means no ad to credit, so route it to organic and move on |
| Booking status | Count kept or completed appointments, not just requests | The value should reflect a real visit, not a no-show or a cancel |
Two rows trip people up.
First, the time window. Allowing a week to catch slow bookers matches calls to bookings that would have happened anyway, so Google Ads gets credit for organic demand. Keep it to a day or two. You will miss a few late bookings; that beats crediting the ad with bookings it did not drive.
Second, the phone number. You do not need the raw number to match, and in healthcare you do not want it in your pipeline. Hash it with SHA-256 on both sides, normalizing first (strip formatting, force E.164) so one number always gives one hash.
// normalize to E.164, then hash. The raw number never leaves this function.
const digits = phone.replace(/\D/g, "");
const e164 = digits.startsWith("1") ? `+${digits}` : `+1${digits}`;
const phoneHash = crypto.createHash("sha256").update(e164).digest("hex");Now the two records join on phoneHash with no raw phone number crossing systems.
Build the pipeline, step by step
The loop, from click to uploaded conversion:
- Capture the gclid on click. CallRail attaches it to the visitor's call session. Confirm it with a test call first.
- Pull calls daily. Fetch the previous day's calls from the CallRail API, keeping the gclid, the timestamp and the number you will hash.
- Hash both sides with the same normalize-then-hash step.
- Match inside the window. Join on
phoneHash. Keep bookings made within 1 to 2 days after the call, and only those that reached a real appointment status. - Assign a value. Take it from your own booking data, for example the service booked multiplied by its list price, computed on your side and in aggregate. Google gets a number, not a patient record.
- Upload through the Data Manager API. Send each matched call to a dedicated
UPLOAD_CLICKSconversion action, with the gclid, value and conversion time. - Reconcile. Log what you uploaded and compare it with Google Ads a day later, so a silent failure surfaces fast.
The upload payload is deliberately thin:
{
"destinations": [{
"operatingAccount": { "accountType": "GOOGLE_ADS", "accountId": "CUSTOMER_ID" },
"productDestinationId": "ACTION_ID"
}],
"events": [{
"adIdentifiers": { "gclid": "<gclid-captured-by-callrail>" },
"eventTimestamp": "2026-07-28T14:11:02+00:00",
"conversionValue": 180,
"currency": "USD"
}]
}The pipeline is the same offline conversion tracking setup I use for web bookings; the phone loop feeds a second event type into it.
The double-counting trap
CallRail's native Google Ads integration posts calls from your Google ads to its own "Phone Call" conversion action, and CallRail recommends making it primary. Separately, you build the pipeline above and upload your own matched-to-booking calls into a "qualified call" action. Now the same call can hit Google twice.
Google Ads de-duplicates inside one action (by order id, if you set one), but two separate actions are two different events to Google. Your conversion count climbs, your cost-per-conversion drops, and smart bidding chases a number that is partly fictional.
The fix is a decision, not more code. Pick one source of truth for phone conversions:
- Keep your own upload as primary, because it is matched to an actual booking, not just a call that rang long enough. Set CallRail's native action to secondary, or turn it off after a couple of weeks of proof.
- Or, if you do not yet trust the custom pipeline, leave the native import as primary and set your own action to secondary and observation-only while you validate it.
Never run two primary actions for the same call: one counts the ring, the other the booking, and they overlap on every lead that did both.
Send calls and web bookings down one pipe
A caller can also be a web booker: someone phones Monday, then books online Wednesday from the same ad click. If your call loop and web-booking loop never talk, you upload two conversions for one gclid and count one person twice.
Route both through the same Data Manager API path, keyed by gclid, even though they land in separate conversion actions. The call goes to your UPLOAD_CLICKS action and the web booking to its own, but both pass through code that can see "this gclid already produced a booking today, do not fire again." Two conversion actions for reporting, one pipeline for truth.
What actually leaves your system
Run this way, only an anonymous click id (the gclid), a conversion value, and a timestamp reach Google. The phone number is hashed before use and never uploaded. No name, no appointment detail, and no patient record is persisted into the attribution files or shipped to the ad platform. That is the posture a HIPAA-conscious practice needs.
When the phone actually rings
Once calls carry a source, patterns show up, such as a lunchtime peak, a dead Sunday, or a pool of calls with no source.
Source quality matters as much as volume: the largest source can be the noisiest.
| Source | Calls | Avg sec | Short-call share | First-time share |
|---|---|---|---|---|
| Business Profile | 612 | 98 | 31% | 68% |
| Website number pool | 287 | 146 | 15% | 60% |
| Paid search | 164 | 132 | 14% | 57% |
| No source | 203 | 88 | 36% | 71% |
Synthetic figures, not client data. Note the no-source row: 203 calls a month with nothing to attribute them to, and the highest short-call share.
When is this worth building?
If phone is a rounding error in your lead mix, skip it and track forms well. Once calls are a meaningful share of qualified leads, bidding works from only part of the picture.
You can build this yourself: CallRail's API, a scheduled job, a hash function, and the Data Manager API are all documented.
Sources checked on 2026-10-25.
Tags
Frequently asked questions
Does Google Ads de-duplicate calls between CallRail's native import and my own conversion action?
No. Google treats two distinct conversion actions as two separate events, so the same call can count in both. Pick one to feed bidding and set the other to observation, or turn the native import off once your own upload is trusted.
How do I match a phone call to a booking?
Join the call to the booking on a hash of the phone number, then keep only matches inside a tight time window of a day or two. A wider window starts pulling in organic bookings that had nothing to do with the ad call.
Do I need the raw phone number to attribute a call?
No. Hash the number with SHA-256 at both ends and match on the hash. The raw number never has to be stored, which matters when the caller is a patient.
Should the native CallRail-to-Google-Ads integration stay on?
Only if it is not competing with your own upload for the same call. Run one as primary for bidding and the other as observation, or retire the native import once your qualified-call upload proves out. Two primaries on one call inflate conversions.
What actually leaves my system when I upload a call conversion?
Only an anonymous click id (the gclid), a conversion value, and a timestamp. The phone number is hashed and the patient's details stay put, so you get Google Ads attribution without shipping personal data to the ad platform.
Why upload calls to the same Data Manager API path as web bookings?
So a caller who later books online is not counted as two customers. Sending both event types through one pipeline, keyed by the same gclid, keeps one lead as one conversion instead of double-firing.
Written by
Marketing Analytics & Conversion Tracking Engineer, US and Canada, run remotely
- Media buyer on Google Ads and Meta Ads from October 2023 to September 2025, nearly $700K in spend.
- Set up patient conversion tracking for a New York medical practice (anonymized).
- Upwork Top Rated Plus, 5.0 from 20 reviews.
