The AI Trades
Call & Lead Handling

Google LSA AI Auto-Responder (Google Ads API)

Replit Build 4 hours
Google LSAGoogle AdsChatGPTReplit

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

Input

New unreplied LSA message leads pulled from the Google Ads API local_services_lead resource

Transformation

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.

Output

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.

ReplacesLSA autoresponder services ($49-99/mo)

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:

MetricBefore (manual)After (automated)
Average response to LSA message leads4-24 hoursUnder 20 minutes
Message lead reply rate20-40%95%+
Google LSA ad rankDowngraded for slow repliesRewarded for fast replies
Booking rate on message leads5-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

RequirementDetailsCost
Active Google LSA accountWith message leads enabledExisting
Google Ads accountLSA must be linked to a Google Ads manager accountFree
Google Ads API developer tokenApply at developers.google.com/google-adsFree
OAuth 2.0 clientSet up in Google Cloud ConsoleFree
Replit account or cron hostFor running the polling scriptFree-$10/mo
OpenAI API keyGPT-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:

FieldWhat 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:

FieldWhat 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.

ConditionDetectionAction
Last message is from you`participant_type == "ADVERTISER"` on latest eventSkip
Lead is WIPED_OUT`contact_details` is nullSkip. Log for manual review.
Already replied to this messageMessage ID in SQLiteSkip
Outside service areaContact details include ZIP not in allow listSkip. Alert owner.
Explicit human takeover flagScript config includes a pause flagSkip entire loop
Rate limit hitGoogle Ads API returns RESOURCE_EXHAUSTEDBack off 5 min, retry

---

13. Testing Checklist

#TestExpected ResultPass?
1Developer token approvedCan run queries against real account
2OAuth refresh token worksScript authenticates without browser prompt
3Fetch recent MESSAGE leads query runsReturns 0+ leads without errors
4Fetch conversation history for a real leadReturns all messages in order
5`needs_reply` correctly identifies new messagesTrue for unreplied, false for already-replied
6Generate reply with ChatGPTReply under 400 chars, plain text
7AppendLeadConversation call succeedsAPI returns success
8Reply appears in LSA dashboardWithin 15 min of API call
9Customer receives reply notificationLSA push notification fires on their end
10Script handles API rate limit gracefullyRetries or alerts, does not crash
11SQLite prevents duplicate repliesSame lead not replied to twice
12Stop conditions block out-of-area leadsFilter 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

ItemCost
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

OptionCost at 20 Leads/Mo
This recipe$7/mo
LSArespond$49-99/mo
PrimeLSA$79+/mo
Manual checking the LSA inboxLost 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

RolesOwner
PrinciplesSpeed Wins the Job