Google LSA AI Auto-Responder (Google Ads API)
The Problem
Google Local Services Ads message leads sit unanswered for hours or days because there is no webhook, no Zapier app, and no push notification you can trust. This is the hardest of the three lead-platform auto-responders to build, which is exactly why nobody is doing it well. This recipe polls the Google Ads API every 2-3 minutes using the local_services_lead resource, detects new unreplied message leads, generates an AI reply with ChatGPT, then writes it back through LocalServicesLeadService.AppendLeadConversation so it shows up in the customer's LSA inbox. Runs on Replit or any cron host. End-to-end latency is 5-20 minutes (Google's internal sync is ~15 min) vs. a manual baseline of 4-24 hours. The ROI is the best of any paid lead channel.
How It Works
New unreplied LSA message leads pulled from the Google Ads API local_services_lead resource
Python script polls every 2-3 min, pulls conversation history via local_services_lead_conversation, sends it to ChatGPT with your business brief, then posts the generated reply back through AppendLeadConversation.
Every LSA message lead gets a professional AI-written reply inside the Google sync interval. Response time drops from hours to under 20 minutes. LSA ad rank improves as Google rewards fast responders.
PRD
# Product Requirements Document
Recipe 113 — Google LSA AI Auto-Responder (Google Ads API)
THE AI TRADES Platform
---
Recipe Slug: `google-lsa-ai-auto-responder`
Recipe Number: 113
Rank: 113 | Tier: 3
Difficulty: Replit Build
Time Estimate: 4 hours
Category: Call & Lead Handling
Software: Google LSA, Google Ads, ChatGPT
Roles: Owner
Trades: All
Principles: Speed Wins the Job
---
1. Recipe Overview
Google Local Services Ads (LSA) are the pay-per-lead placements that sit at the top of Google search for "plumber near me" style queries. They come in two flavors: phone call leads and message leads. Phone call leads ring your business line directly. Message leads land in the LSA inbox and wait for you to reply, which you almost never do fast enough.
This recipe builds a polling script that hits the Google Ads API, pulls new LSA message leads every 2-5 minutes, generates an AI reply with ChatGPT, and writes the reply back through the `AppendLeadConversation` API method. The reply shows up in the LSA messaging interface as if you typed it yourself.
Input: New unreplied LSA message leads (pulled via Google Ads API `local_services_lead` resource)
Transformation: Script detects new messages, sends the content to ChatGPT with your business brief, posts the generated reply back to LSA via `LocalServicesLeadService.AppendLeadConversation`
Output: Every LSA message lead gets an AI-written reply within the Google sync interval (~15 minutes), with the first message posted in under 5 minutes of the lead arriving.
---
2. The Problem
LSA Message Leads Are the Slowest Channel You Have
Google LSA is an expensive lead source. Plumbing, HVAC, and roofing leads regularly cost $50-$150 per. The difference between LSA and Angi or Yelp is that LSA is Google, which means these customers already trust Google enough to click your listing. Conversion rates are high when you respond fast.
The problem:
- LSA has no reply reminder, no push notification you can trust, and no webhook
- The LSA dashboard is buried in Google Ads
- Most contractors treat LSA as phone-call-only and never reply to message leads
- Message leads sit unanswered for hours or days
- Google downgrades your ad rank when you ignore message leads
Why is there no easy fix?
Google does not offer a webhook. There is no Zapier trigger app. The only way to get real-time-ish access to LSA message leads is the Google Ads API, which requires a developer token and OAuth setup. This is the hardest integration of the three major lead platforms, which is exactly why nobody is doing it, which is exactly why it is such a high-leverage build.
---
3. The Solution
A Python script (or n8n workflow, or Replit-hosted service) that does three things on a 2-5 minute loop:
1. Polls the Google Ads API using the `local_services_lead` resource, filtered to leads created in the last 24 hours with no reply from the business
2. For each unreplied message lead, pulls the `local_services_lead_conversation` history and sends the context to ChatGPT to generate a reply
3. Writes the reply back via `LocalServicesLeadService.AppendLeadConversation`, which Google then syncs to the LSA inbox (~15 min sync interval)
Hosted on Replit or any cron-capable server. Fully autonomous. No email parsing, no webhook hacks, no reliance on Zapier's limited catalog.
Expected results:
| Metric | Before (manual) | After (automated) |
|---|---|---|
| Average response to LSA message leads | 4-24 hours | Under 20 minutes |
| Message lead reply rate | 20-40% | 95%+ |
| Google LSA ad rank | Downgraded for slow replies | Rewarded for fast replies |
| Booking rate on message leads | 5-10% | 20-30% |
Important caveat: Google's internal sync interval for `AppendLeadConversation` is approximately 15 minutes. Your script runs faster than that, but the customer sees the reply in their LSA interface after the next sync. Practical latency: 5-20 minutes end to end, vs. 4-24 hours manual. Still a massive improvement.
---
4. Prerequisites
| Requirement | Details | Cost |
|---|---|---|
| Active Google LSA account | With message leads enabled | Existing |
| Google Ads account | LSA must be linked to a Google Ads manager account | Free |
| Google Ads API developer token | Apply at developers.google.com/google-ads | Free |
| OAuth 2.0 client | Set up in Google Cloud Console | Free |
| Replit account or cron host | For running the polling script | Free-$10/mo |
| OpenAI API key | GPT-4o-mini works | ~$0.0002 per reply |
Critical: Google Ads API developer token approval takes 1-5 business days. You apply, they review, you get either "test access" (limited to test accounts) or "standard access" (production). For this recipe you need standard access. The application is a short form explaining your use case.
Alternative if you cannot get API access: LSArespond and PrimeLSA are third-party services that already have API access and charge a monthly fee. If the setup below feels too heavy, those are fine options. This recipe is for contractors who want to own the stack.
---
5. Google Ads API Setup
Step 1: Apply for a Developer Token
1. Go to `https://developers.google.com/google-ads/api/docs/get-started/dev-token`
2. Sign in with the Google account that owns your Google Ads manager account
3. Fill out the application. Use language like:
```
Use case: Internal automation for a single Google LSA account.
Purpose: Auto-reply to inbound LSA message leads using the
AppendLeadConversation method to improve customer response times.
No third-party data handling, no resale of data, single-account use.
```
4. Submit. Wait 1-5 business days for approval.
Step 2: Create OAuth 2.0 Credentials
1. Go to `console.cloud.google.com`
2. Create a new project: "LSA Auto Responder"
3. Enable the Google Ads API
4. Go to APIs & Services → Credentials
5. Create OAuth 2.0 Client ID (Desktop application type)
6. Download the credentials JSON file
Step 3: Generate a Refresh Token
Use the `google-ads` Python client library's sample script:
```bash
pip install google-ads
python -m google.ads.googleads.examples.authentication.generate_user_credentials \
--client_id=YOUR_CLIENT_ID \
--client_secret=YOUR_CLIENT_SECRET \
--additional_scopes=https://www.googleapis.com/auth/adwords
```
The script opens a browser for you to authorize, then prints a refresh token. Save it.
Step 4: Build Your google-ads.yaml Config
```yaml
developer_token: YOUR_APPROVED_DEV_TOKEN
client_id: YOUR_OAUTH_CLIENT_ID
client_secret: YOUR_OAUTH_CLIENT_SECRET
refresh_token: YOUR_REFRESH_TOKEN
login_customer_id: YOUR_GOOGLE_ADS_MANAGER_ID
use_proto_plus: True
```
Store this in Replit Secrets or your environment. Never commit it to git.
---
6. Core API Resources You Will Use
`local_services_lead`
Represents a single LSA lead. Key fields:
| Field | What It Holds |
|---|---|
| `resource_name` | Unique ID for the lead |
| `lead_type` | `PHONE_CALL`, `MESSAGE`, or `BOOKING` |
| `lead_status` | Current state |
| `category_id` | LSA category (plumber, HVAC, etc.) |
| `service_id` | Specific service requested |
| `contact_details` | Name, phone, email (nulled when status is WIPED_OUT) |
| `creation_date_time` | When the lead arrived |
| `lead_charged` | Whether Google already charged you |
Note: For this recipe, filter to `lead_type = 'MESSAGE'`. Phone call leads cannot be replied to programmatically.
`local_services_lead_conversation`
Represents one message in a lead's conversation. Each lead has a one-to-many relationship with conversations. Key fields:
| Field | What It Holds |
|---|---|
| `lead` | FK to parent lead |
| `conversation_channel` | MESSAGE or PHONE_CALL |
| `participant_type` | CONSUMER or ADVERTISER |
| `event_date_time` | When the message was sent |
| `message_details.text` | The actual message content |
| `message_details.attachment_urls` | Any attached files |
| `phone_call_details.call_duration_millis` | For phone call leads |
| `phone_call_details.call_recording_url` | Authenticated call recording |
`LocalServicesLeadService.AppendLeadConversation`
The write method. Takes a lead resource name and a new conversation event. Posts the new event into the lead's conversation history, which Google then syncs to the LSA inbox.
---
7. Polling Script Architecture
```
┌──────────────────────────────────────┐
│ Cron trigger (every 3 min) │
└─────────────┬────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Query: local_services_lead │
│ WHERE lead_type = MESSAGE │
│ AND creation_date_time >= -24h │
└─────────────┬────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ For each lead: │
│ Check local store (SQLite): │
│ Has latest consumer message been │
│ replied to? │
└─────────────┬────────────────────────┘
│ (new message found)
▼
┌──────────────────────────────────────┐
│ Query: local_services_lead_ │
│ conversation WHERE lead = X │
│ Returns full history │
└─────────────┬────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Send history + business brief to │
│ ChatGPT (gpt-4o-mini) │
│ Get back: reply_text │
└─────────────┬────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Call AppendLeadConversation with │
│ lead resource + new MESSAGE event │
└─────────────┬────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Log to SQLite: │
│ lead_id, reply_text, timestamp │
└──────────────────────────────────────┘
```
---
8. Pseudo-Code for the Main Loop
```python
from google.ads.googleads.client import GoogleAdsClient
from openai import OpenAI
import sqlite3
from datetime import datetime, timedelta
client = GoogleAdsClient.load_from_storage("google-ads.yaml")
openai = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
db = sqlite3.connect("lsa_state.db")
BUSINESS_BRIEF = """
(Your system prompt from Section 9 goes here)
"""
def fetch_recent_message_leads(customer_id):
ga_service = client.get_service("GoogleAdsService")
cutoff = (datetime.utcnow() - timedelta(hours=24)).isoformat()
query = f"""
SELECT
local_services_lead.resource_name,
local_services_lead.id,
local_services_lead.creation_date_time,
local_services_lead.lead_type,
local_services_lead.contact_details.name,
local_services_lead.category_id
FROM local_services_lead
WHERE local_services_lead.lead_type = 'MESSAGE'
AND local_services_lead.creation_date_time >= '{cutoff}'
"""
return ga_service.search(customer_id=customer_id, query=query)
def fetch_conversation_history(customer_id, lead_resource):
ga_service = client.get_service("GoogleAdsService")
query = f"""
SELECT
local_services_lead_conversation.event_date_time,
local_services_lead_conversation.participant_type,
local_services_lead_conversation.message_details.text
FROM local_services_lead_conversation
WHERE local_services_lead_conversation.lead = '{lead_resource}'
ORDER BY local_services_lead_conversation.event_date_time ASC
"""
return list(ga_service.search(customer_id=customer_id, query=query))
def needs_reply(history):
if not history:
return False
last = history[-1]
return last.local_services_lead_conversation.participant_type == "CONSUMER"
def generate_reply(history, customer_name):
messages = [{"role": "system", "content": BUSINESS_BRIEF}]
for entry in history:
role = "user" if entry.participant_type == "CONSUMER" else "assistant"
text = entry.message_details.text
messages.append({"role": role, "content": text})
messages.append({
"role": "user",
"content": f"Write the next reply to {customer_name}. Max 400 chars. Plain text."
})
resp = openai.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
max_tokens=200
)
return resp.choices[0].message.content.strip()
def append_reply(customer_id, lead_resource, reply_text):
service = client.get_service("LocalServicesLeadService")
conversation = client.get_type("LocalServicesLeadConversation")
conversation.lead = lead_resource
conversation.participant_type = "ADVERTISER"
conversation.message_details.text = reply_text
request = client.get_type("AppendLeadConversationRequest")
request.customer_id = customer_id
request.conversations.append(conversation)
return service.append_lead_conversation(request=request)
def run_poll(customer_id):
leads = fetch_recent_message_leads(customer_id)
for row in leads:
lead_res = row.local_services_lead.resource_name
if already_replied(db, lead_res):
continue
history = fetch_conversation_history(customer_id, lead_res)
if not needs_reply(history):
continue
reply = generate_reply(history, row.local_services_lead.contact_details.name)
append_reply(customer_id, lead_res, reply)
log_reply(db, lead_res, reply)
if __name__ == "__main__":
run_poll(os.environ["GOOGLE_ADS_CUSTOMER_ID"])
```
This is intentionally not production-perfect. Treat it as a starting skeleton. You still need to:
- Add retry logic for API rate limits
- Handle the WIPED_OUT status (leads with nulled contact info)
- Add a `last_seen` timestamp per lead so you do not re-process
- Handle multi-message bursts
- Add error reporting (email, Slack, whatever)
---
9. Business Brief (System Prompt)
```
You are the front desk for [COMPANY NAME], a [TRADE] contractor
serving [CITY/REGION]. You are replying to Google Local Services Ads
(LSA) message leads.
Rules:
1. Plain text only. No markdown, no emoji, no URLs.
2. Maximum 400 characters per reply.
3. Use the customer's first name once if you have it.
4. Reference the specific issue or service they asked about. Prove
you read what they wrote.
5. Never quote a specific price. Say you need to see it or ask a
clarifying question.
6. Ask for the best way to reach them. Google LSA messages are slow,
so pushing toward a phone call or text is usually better.
7. If they are outside our service area, politely decline and mention
why.
8. Never promise same-day service unless it is after 8am and before
2pm local time. We cannot deliver on late-day same-day promises.
9. Sound like a real local contractor. Drop corporate language like
"I would be delighted," "thank you for your inquiry," or "at your
earliest convenience."
10. End with your first name and the company name.
Company: [COMPANY NAME]
Your name: [OWNER/CSR FIRST NAME]
Phone: [PHONE]
Service area: [LIST]
Specialties: [LIST]
Hours: [HOURS]
Things we do not do: [LIST]
```
---
10. Example Exchange
LSA message thread (incoming):
```
[Jessica — CONSUMER — 10:14 AM]
Hi, I need my dryer vent cleaned. It has been over a year and the
dryer is taking forever to dry. Can you help?
```
Generated reply (posted via AppendLeadConversation):
```
Hi Jessica. Yes we do dryer vent cleaning, and a year is usually when
it starts to show. We check for buildup, lint clogs, and any damaged
venting. Quick question so I can price it right: do you have a first
floor or second floor dryer, and is the vent exterior or through the
roof? Easiest to call or text — what is a good number? — Mike at
Austin Vent Pros
```
- Character count: 385
- Delivery: AppendLeadConversation succeeded at 10:16 AM
- Customer saw it in LSA inbox at 10:28 AM (next Google sync)
- Real-world latency: 14 minutes
- Manual baseline: 6+ hours
---
11. Hosting on Replit
Replit is the easiest host for this build. Here is the setup:
1. Create a new Python Repl: "LSA Auto Responder"
2. Paste the script from Section 8 (plus your production fills)
3. Add secrets in the Replit Secrets tab:
- `OPENAI_API_KEY`
- `GOOGLE_ADS_CUSTOMER_ID`
- `GOOGLE_ADS_DEVELOPER_TOKEN`
- (and the rest of your google-ads.yaml fields)
4. Enable "Always On" (paid Replit feature, ~$7/mo)
5. Use Replit's Scheduled Deployments or a `while True` loop with `time.sleep(180)`
For a more robust setup, use a proper cron host (GitHub Actions, Railway, Render) and trigger the script every 3 minutes.
---
12. Stop Conditions
Do NOT reply in these cases. Build these as checks in the polling loop.
| Condition | Detection | Action |
|---|---|---|
| Last message is from you | `participant_type == "ADVERTISER"` on latest event | Skip |
| Lead is WIPED_OUT | `contact_details` is null | Skip. Log for manual review. |
| Already replied to this message | Message ID in SQLite | Skip |
| Outside service area | Contact details include ZIP not in allow list | Skip. Alert owner. |
| Explicit human takeover flag | Script config includes a pause flag | Skip entire loop |
| Rate limit hit | Google Ads API returns RESOURCE_EXHAUSTED | Back off 5 min, retry |
---
13. Testing Checklist
| # | Test | Expected Result | Pass? |
|---|---|---|---|
| 1 | Developer token approved | Can run queries against real account | ☐ |
| 2 | OAuth refresh token works | Script authenticates without browser prompt | ☐ |
| 3 | Fetch recent MESSAGE leads query runs | Returns 0+ leads without errors | ☐ |
| 4 | Fetch conversation history for a real lead | Returns all messages in order | ☐ |
| 5 | `needs_reply` correctly identifies new messages | True for unreplied, false for already-replied | ☐ |
| 6 | Generate reply with ChatGPT | Reply under 400 chars, plain text | ☐ |
| 7 | AppendLeadConversation call succeeds | API returns success | ☐ |
| 8 | Reply appears in LSA dashboard | Within 15 min of API call | ☐ |
| 9 | Customer receives reply notification | LSA push notification fires on their end | ☐ |
| 10 | Script handles API rate limit gracefully | Retries or alerts, does not crash | ☐ |
| 11 | SQLite prevents duplicate replies | Same lead not replied to twice | ☐ |
| 12 | Stop conditions block out-of-area leads | Filter works | ☐ |
Tip: You can test the full loop against your own LSA account by having a friend use Google search for your service, click your ad, and message you through LSA. The cost is whatever the lead charge is for your category.
---
14. Cost Breakdown
Monthly at 20 LSA Message Leads
| Item | Cost |
|---|---|
| Replit Always On | $7 |
| OpenAI API (GPT-4o-mini, 20 leads + multi-turn) | $0.02 |
| Google Ads API | $0 (free) |
| Total | ~$7/mo |
Compared to Alternatives
| Option | Cost at 20 Leads/Mo |
|---|---|
| This recipe | $7/mo |
| LSArespond | $49-99/mo |
| PrimeLSA | $79+/mo |
| Manual checking the LSA inbox | Lost leads |
Break-even against managed services is immediate. The only cost is the setup time. Once it is running, you never think about LSA message leads again.
---
15. Maintenance and Monitoring
Weekly:
- Check the SQLite log for replies that look weird or wrong
- Verify the script is still running (Replit "Always On" can occasionally stall)
- Review the AI reply quality on 3-5 random leads
Monthly:
- Check your LSA dashboard for response time stats (Google uses this for ad rank)
- Look for new LSA categories or features
- Update your business brief if you add or drop services
Watch for:
- Google Ads API version deprecations (they release new versions yearly)
- OAuth refresh token expiration (usually 6 months of inactivity, but safer to rotate annually)
- Spikes in lead volume that exceed API quotas
Alerts to set up:
- Script error → email or Slack
- API authentication failure → email or Slack
- No leads processed in 24 hours (might be normal, might be a bug) → email
---
16. Upgrade Paths
Once the basic reply loop is running, consider:
1. Lead qualification: Add a keyword or AI-based check that routes low-intent leads ("just looking") to a different template
2. Hand-off to human: After 3 AI turns, flag the conversation for a human CSR to take over
3. Cross-platform unification: Pair with the Yelp and Angi recipes so all three platforms feed into the same CRM record
4. Call transcription integration: Pull LSA phone call recordings (available in `phone_call_details.call_recording_url`) and feed them to Whisper for searchable call history
5. Response time reporting: Push your LSA reply times into a weekly dashboard alongside your other channels
---
Recipe 113 — Google LSA AI Auto-Responder (Google Ads API)
THE AI TRADES Platform
Difficulty: Replit Build | Setup: 4 hours | Category: Call & Lead Handling