If you run Google Ads for a business that gets leads by phone, a large share of your conversions can be invisible to Google. Someone clicks an ad, calls instead of filling in the form, books a few days later, and the ad platform never learns the click worked. The fix is a second attribution loop for phone leads: pull each tracked call, along with the gclid it carried, out of a tool like CallRail, match it to the eventual booking, and upload that as an offline conversion. The one thing that quietly ruins this setup is double-counting, and it is easy to avoid once you know where it hides.
I built this loop for a multi-location US medical practice running Google Ads for both calls and online bookings. Phone was a big channel there, often roughly half of qualified leads, so the web-form conversions Google could see told only part of the story. Below is the pipeline that closed the gap and the trap that would have inflated it.
Quick answer
Build the phone-lead 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 is already importing calls, do not also add a competing "qualified call" conversion action set to primary. Google does not de-duplicate across two separate conversion actions, so the same call lands twice. Pick one to drive 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 sitting in the browser when the form fires. A phone call breaks that chain. The visitor leaves the site, dials a number, and the click id is nowhere near the conversation. Unless something carries the gclid across that gap, Google credits the click with nothing, and your smart bidding optimizes toward the leads it can see (forms) while starving the channel it cannot (calls).
Call-tracking tools close the gap. CallRail swaps in a dynamic number per visitor, ties that number to the session, and captures the gclid from the URL. When the call comes in, CallRail knows which click produced it. That is the raw material for offline attribution: a call event that still remembers its gclid. Your job is to decide which of those calls counts as a conversion, attach a value, and hand it back to Google.
How do I match a call to a booking?
A ringing phone is not a conversion. In a medical practice, plenty of calls are existing patients, wrong numbers, or people who never book. You want to credit the ad only when a call turns into a real appointment, which means joining two systems: the call log and the booking record.
Match on these signals, in this order.
| 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 of those rows are where people go wrong.
First, the time window. It is tempting to allow a week so you catch the slow bookers. Do not. A week is long enough that you start matching calls to bookings that would have happened anyway, and you end up crediting Google Ads for organic demand. Keep it to a day or two. You will miss a few genuine late bookings, and that is the correct trade: under-crediting a little beats inflating your cost-per-acquisition math with bookings the ad did not drive.
Second, the phone number. You do not need the raw number to match, and in healthcare you actively do not want it in your pipeline. Hash it with SHA-256 on both sides and compare hashes. Normalize first (strip formatting, force E.164) so the same number always produces the same 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 call record and the booking record can be joined on phoneHash with no personal data crossing between systems.
Build the pipeline, step by step
Here is the full loop, from click to uploaded conversion.
- Capture the gclid on click. Your landing page reads the gclid from the URL, and CallRail attaches it to the visitor's tracked call session. This is CallRail's job; confirm it in a test call before trusting anything downstream.
- Pull calls on a schedule. Once a day, fetch the previous day's calls from the CallRail API, keeping the gclid, timestamp, call duration, and the phone number you are about to hash.
- Hash both sides. Run the same normalize-then-hash step on the call's number and on each booking's number, so the two datasets share a join key.
- Match inside the window. Join call to booking on
phoneHashwhere the booking falls within 1 to 2 days after the call, and only for bookings that reached a real appointment status. - Assign a value. Pull the value 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, carrying the gclid, the value, and the conversion timestamp. - Reconcile. Log what you uploaded and diff it against what appears in Google Ads a day later, so a silent failure surfaces fast instead of a month later.
The upload payload is deliberately thin:
{
"conversionAction": "customers/CUSTOMER_ID/conversionActions/ACTION_ID",
"gclid": "<gclid-captured-by-callrail>",
"conversionDateTime": "2026-07-28 14:11:02+00:00",
"conversionValue": 180,
"currencyCode": "USD"
}That is everything Google needs and nothing it does not. This is the same offline conversion tracking setup I use for web bookings; the phone loop just feeds a second event type into it.
The double-counting trap
Here is the mistake that inflates a phone-lead setup, and it is subtle because both halves look correct on their own.
CallRail ships a native Google Ads integration. Turn it on, and CallRail pushes qualified calls straight into a Google Ads conversion action for you. Separately, you build the pipeline above and upload your own matched-to-booking calls into a "qualified call" action. Now the same phone call can hit Google twice: once from CallRail's native import, once from your upload. Google Ads does not de-duplicate across two distinct conversion actions. It de-duplicates inside one action (by order id, if you set one), but two separate actions are, as far as Google is concerned, two different things that happened. Your conversion count climbs, your cost-per-conversion drops, and smart bidding starts chasing 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 (it feeds bidding) 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 entirely once your upload has proven out for a couple of weeks.
- Or, if you are not ready to trust the custom pipeline yet, leave the native import as primary and set your own action to secondary and observation-only while you validate it.
What you must not do is run two primary actions for the same call. One counts the ring, the other counts the booking, and they overlap on every lead that did both. Decide which event you are actually paying for, make that one primary, and demote the other.
Send calls and web bookings down one pipe
A caller can also be a web booker. Someone phones on Monday, does not commit, then books online on Wednesday from the same ad click. If your call loop and your web-booking loop are separate systems that never talk, you can upload two conversions for one gclid and count one person as two customers.
Avoid it by routing both event types through the same Data Manager API path, keyed by gclid, even though they land in separate conversion actions. One uploader, one place where de-duplication by gclid can happen, one definition of a conversion. The call goes to your UPLOAD_CLICKS qualified-call action and the web booking goes to its own action, but they pass through the same 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
This matters in healthcare, and it is a selling point everywhere else. When you run the loop this way, the only things that ever leave your environment and reach Google are an anonymous click id (the gclid), a conversion value, and a timestamp. The phone number is hashed before it is used and is never uploaded. No name, no appointment detail, and no patient record is persisted into the attribution files or shipped to the ad platform. You get full Google Ads attribution on phone leads while the sensitive data stays inside your own systems, exactly the posture a HIPAA-conscious practice needs.
When the phone actually rings
Once calls carry a source, the patterns show up. A weekday lunchtime peak, a dead Sunday, and a big pool of calls that arrive with no source attached at all.
Source quality matters as much as volume. The largest source is frequently the noisiest, and an unattributed pool is money you cannot yet optimize.
| 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, proportions typical. Note the no-source row: 203 calls a month with nothing to attribute them to, and the highest short-call share of the lot.
When is this worth building?
If phone is a rounding error in your lead mix, skip it and track forms well. The moment calls are a meaningful share of qualified leads, and for a lot of local and medical advertisers they are close to half, the blind spot is costing you. Bidding is optimizing on half the picture, and the channel driving real appointments looks weaker than the one driving form fills.
You can build this yourself: CallRail's API, a scheduled job, a hash function, and the Data Manager API are all documented. If you would rather it just work, wiring call-tracking into Google Ads without the double-counting is a core part of the offline conversion tracking I do. Send me your current setup and how you book appointments, and I will tell you whether it is a clean afternoon or a real project before either of us commits.
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 and you are keeping PHI out of the pipeline.
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.
Need something like this built?
Free 15-min discovery call. I'll listen, ask honest questions, and tell you if I can help.