DataLatte☕
AI Agent for Google Reviews: Python Reply Script
AI & Automation

AI Agent for Google Reviews: Python Reply Script

Part of the guide: Local SEO & Google Business Profile

June 13, 2026·Nataliia· 10 min read All posts
Posting a reply to a Google review through the API is a single request. Getting permission to send that request is the hard part: Google requires a verified Business Profile that has been active for 60 or more days, a website on the profile, and an approved "Application for Basic API Access".
Reading reviews is also one request, returning up to 50 per page, and a reply can be at most 4,096 bytes. Everything else in this article is a Python script that wraps those two calls, drafts replies with a language model, and refuses to post anything risky.
Untested starting point
The script below is a template written from Google's and Groq's published documentation. It has not been run against a live Business Profile. Read it, run it with DRY_RUN=1 (the default) and test on your own reviews before you let it post anything. Treat it as a starting point, not a finished product.
This page covers the code. For the strategy side (what Google and the FTC allow, which reviews a human should always handle), read our AI agent for Google reviews management guide first. For wording that works on bad reviews, see our negative review reply scripts. It is written for owners and developers serving customers in the US, UK, Australia and Canada.

Get Access First

Per Google's prerequisites page, you need:
  1. A Google Cloud project.
  2. A verified Business Profile, yours or a client's, that has been active for 60 or more days, with a website listed on it.
  3. A submitted "Application for Basic API Access" through Google's contact form, with your project number, from an email address that owns or manages the profile.
  4. After approval, the Business Profile APIs enabled for the project.
You can check approval in the Cloud console: Google says a quota of 0 queries per minute means you are not yet approved, and 300 means you are.
Google's usage limits page adds that a quota of 0 means you should submit the access application rather than request a quota increase, and that requests over quota return HTTP 429, for which it recommends exponential backoff with jitter.
Its review data page also says that to use the reply functionality, a Google Workspace administrator must enable Search and/or Maps with Business Profile for the account. Google's basic setup page says Workspace accounts without Business Profile enabled get a 403 error.
The review endpoints we use live in the older v4 API on mybusiness.googleapis.com. Google's REST reference for listing reviews and replying shows no deprecation notice for these methods at the time of writing, but the docs link to a general deprecation schedule, so check it before you build anything long-lived.
If this sounds like a lot of admin for a coffee shop, it is. Many small businesses use a hosted tool that already has approval. Our AI review management service works that way.

How the Script Works

Refresh OAuth access token
        |
GET reviews (50 per page, newest first)
        |
Skip any review that already has a reviewReply
        |
Stop words (refund, lawyer, injured ...) -> flag for the owner, no draft
        |
Draft a reply with the LLM (model may answer HOLD -> flag for the owner)
        |
1-3 stars, or DRY_RUN on -> print the draft, post nothing
        |
4-5 stars and DRY_RUN off -> PUT the reply, log the review
Design choices worth copying even if you rewrite the code:
  • Dry run is the default. Nothing posts until you set DRY_RUN=0.
  • Only 4 to 5 star reviews can ever be auto-posted. Everything lower is draft-only.
  • The model can say HOLD. The prompt tells it to output HOLD for health, money, legal or safety content, and the script treats that as "owner decides".
  • No generic fallback reply. If the model fails, the script skips the review rather than posting boilerplate.
  • A log file prevents double replies.

Find Your Account and Location IDs

Google's basic setup page shows listing accounts with a GET to the Account Management API. Its locations list reference says the Business Information API needs a readMask. Replace ACCESS_TOKEN with a token from the OAuth flow (scope https://www.googleapis.com/auth/business.manage):
# 1. List accounts (the "name" looks like accounts/123...)
curl -H "Authorization: Bearer ACCESS_TOKEN" \
  "https://mybusinessaccountmanagement.googleapis.com/v1/accounts"

# 2. List that account's locations (the "name" looks like locations/456...)
curl -H "Authorization: Bearer ACCESS_TOKEN" \
  "https://mybusinessbusinessinformation.googleapis.com/v1/accounts/ACCOUNT_ID/locations?readMask=name,title"
The v4 review path combines the two: accounts/ACCOUNT_ID/locations/LOCATION_ID. That is the value for LOCATION_NAME below. The review resource's own name field comes back in the form accounts/{accountId}/locations/{locationId}/reviews/{reviewId}, which is what the reply call needs.

The Script

Environment variables hold secrets. Never commit them. The Groq endpoint and response shape follow Groq's API reference, where llama-3.3-70b-versatile is the example model; check Groq's models page for the current list. Any OpenAI-style chat provider works with small changes.
#!/usr/bin/env python3
"""Draft (and optionally post) replies to Google reviews.
UNTESTED STARTING POINT. Run with DRY_RUN=1 first and read every draft."""

import json
import os
import re
import urllib.parse
import urllib.request
from datetime import datetime, timezone
from pathlib import Path

GROQ_API_KEY = os.environ["GROQ_API_KEY"]
GROQ_MODEL = os.environ.get("GROQ_MODEL", "llama-3.3-70b-versatile")
CLIENT_ID = os.environ["GOOGLE_CLIENT_ID"]
CLIENT_SECRET = os.environ["GOOGLE_CLIENT_SECRET"]
REFRESH_TOKEN = os.environ["GOOGLE_REFRESH_TOKEN"]
LOCATION_NAME = os.environ["LOCATION_NAME"]  # accounts/ACCOUNT_ID/locations/LOCATION_ID
BUSINESS_NAME = os.environ.get("BUSINESS_NAME", "our business")
OWNER_NAME = os.environ.get("OWNER_NAME", "The team")
DRY_RUN = os.environ.get("DRY_RUN", "1") != "0"  # anything but "0" means dry run

LOG_FILE = Path("replied_reviews.json")
MAX_PAGES = 3  # 3 pages x 50 reviews; raise if you have a backlog
STARS = {"ONE": 1, "TWO": 2, "THREE": 3, "FOUR": 4, "FIVE": 5}
STOP_WORDS = re.compile(
    r"refund|lawyer|attorney|sue|injur|sick|poison|police|discriminat|allerg",
    re.IGNORECASE,
)


def call(url, method="GET", headers=None, body=None, timeout=20):
    data = body if isinstance(body, bytes) else (json.dumps(body).encode() if body else None)
    req = urllib.request.Request(url, data=data, method=method, headers=headers or {})
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        raw = resp.read()
    return json.loads(raw) if raw else {}


def get_access_token():
    body = urllib.parse.urlencode({
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "refresh_token": REFRESH_TOKEN,
        "grant_type": "refresh_token",
    }).encode()
    resp = call("https://oauth2.googleapis.com/token", "POST",
                {"Content-Type": "application/x-www-form-urlencoded"}, body)
    return resp["access_token"]


def fetch_unanswered(token):
    reviews, page_token = [], None
    for _ in range(MAX_PAGES):
        params = {"pageSize": 50}
        if page_token:
            params["pageToken"] = page_token
        url = f"https://mybusiness.googleapis.com/v4/{LOCATION_NAME}/reviews?" + urllib.parse.urlencode(params)
        data = call(url, headers={"Authorization": f"Bearer {token}"})
        reviews += data.get("reviews", [])
        page_token = data.get("nextPageToken")
        if not page_token:
            break
    return [r for r in reviews if "reviewReply" not in r]


def draft_reply(review):
    reviewer = review.get("reviewer", {})
    first_name = ""
    if not reviewer.get("isAnonymous") and reviewer.get("displayName"):
        first_name = reviewer["displayName"].split()[0]
    stars = STARS.get(review.get("starRating"), 0)
    prompt = (
        f"You write Google review replies for {BUSINESS_NAME}. Tone: warm, brief, human.\n"
        f"Rules: use the reviewer's first name if given, mention one specific detail from the review, "
        f"under 90 words, no discounts, no admitting legal fault, no private details, "
        f"sign off as {OWNER_NAME}. For 1-3 stars, thank them, apologise that the visit fell short "
        f"and invite them to contact us directly. If the review mentions health, safety, money or "
        f"legal action, output exactly HOLD. Output only the reply text.\n\n"
        f"First name: {first_name or '(not given)'}\nStars: {stars}\n"
        f"Review: {review.get('comment') or '(no text)'}"
    )
    payload = {
        "model": GROQ_MODEL,
        "messages": [{"role": "user", "content": prompt}],
        "max_completion_tokens": 250,
        "temperature": 0.6,
    }
    try:
        resp = call("https://api.groq.com/openai/v1/chat/completions", "POST",
                    {"Authorization": f"Bearer {GROQ_API_KEY}", "Content-Type": "application/json"},
                    payload)
        text = resp["choices"][0]["message"]["content"].strip()
    except Exception as exc:  # no generic fallback: skip instead
        print(f"  model call failed, skipping: {exc}")
        return None
    if text.upper().startswith("HOLD") or len(text.encode()) > 4096:
        return None
    return text


def post_reply(token, review_name, text):
    url = f"https://mybusiness.googleapis.com/v4/{review_name}/reply"
    call(url, "PUT", {"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
         {"comment": text})


def main():
    log = json.loads(LOG_FILE.read_text()) if LOG_FILE.exists() else {}
    token = get_access_token()
    reviews = fetch_unanswered(token)
    print(f"{len(reviews)} unanswered review(s). DRY_RUN={DRY_RUN}")

    for review in reviews:
        name = review["name"]
        if name in log:
            continue
        stars = STARS.get(review.get("starRating"), 0)
        comment = review.get("comment", "")
        print(f"\n{stars} stars: {comment[:120]!r}")

        if STOP_WORDS.search(comment):
            print("  FLAGGED: stop word found. Owner replies personally.")
            continue
        text = draft_reply(review)
        if text is None:
            print("  No draft (HOLD or error). Owner decides.")
            continue
        print(f"  DRAFT: {text}")

        if DRY_RUN or stars < 4:
            print("  Draft only. Review and post manually.")
            continue
        try:
            post_reply(token, name, text)
        except Exception as exc:
            print(f"  post failed: {exc}")
            continue
        log[name] = {"at": datetime.now(timezone.utc).isoformat(), "stars": stars}
        print("  posted")

    LOG_FILE.write_text(json.dumps(log, indent=2))


if __name__ == "__main__":
    main()
What to check before you rely on it:
  • Moderation. Google's reply help page says each reply is checked against content policies before it posts, usually in about 10 minutes but sometimes up to 30 days, and that a rejected reply must be edited. The review resource includes a reviewReplyState of PENDING, APPROVED or REJECTED and a policyViolation field when a reply is rejected. The script does not read these yet; adding a check is a sensible next step.
  • Error handling. It stops on the first Google error when fetching and does not retry. Add exponential backoff for 429 responses, as Google recommends.
  • Scheduling. Run it once a day from cron or a scheduled GitHub Action, not in a tight loop.
  • Stop words. The list above is a starting point. Add words that matter in your trade.

Example Replies (Illustrative)

These show the tone the prompt aims for. They are examples we wrote, not real customer reviews.
5 stars, from Sarah: "Best balayage I've ever had. Emma understood what I wanted without a million photos." Draft: Hi Sarah, Emma will be delighted to read this. Getting the colour right without a reference board is exactly what we aim for. See you at your next appointment. Nataliia
2 stars, from Mike: "Waited 20 minutes past my appointment time. Good cut but the wait was frustrating." Draft (owner edits, never auto-posted): Hi Mike, you're right that 20 minutes is too long and I'm sorry it happened. Please email or call us so we can look at your booking and make your next visit smoother. Nataliia

The Math

The script is cheap to run; the cost is your time to build and maintain it. Every figure below is an assumption, so substitute your own. Check your model provider's current pricing, which this article does not quote.
QuietTypicalBusy
Reviews per month (assumption)3060120
Minutes saved per reply: 4 by hand vs 1 to review a draft (assumption)333
Hours saved per month1.53.06.0
Value of your time at $30 per hour (assumption)$45$90$180
Upkeep: 1 hour per month (assumption)$30$30$30
Net monthly gain$15$60$150
One-off build and approval time: 6 hours (assumption)$180$180$180
Months to pay back1231.2
Read it honestly. At 30 reviews a month the payback is a year, and a saved-replies document may beat a script. The case gets stronger at volume or across several locations, and the Google access application can add weeks on top.

Do This Today

  1. Check whether your Business Profile is verified, has a website listed and is at least 60 days old.
  2. Create a Google Cloud project, submit the "Application for Basic API Access", and note the project number you used.
  3. Count your unanswered reviews from the last 90 days. That is your volume and your backlog.
  4. Write a voice guide: three replies you are proud of and three phrases you never use. Paste them into the prompt.
  5. When access arrives, run the script with DRY_RUN=1 for a week and compare each draft with what you would have written. Only then consider DRY_RUN=0, and keep 1 to 3 star reviews manual.

FAQ

Q: Is it allowed to post AI-written replies to Google reviews? A: Google's help pages say replies are checked against its content policies and that unapproved replies must be edited. We have not found a Google rule that specifically bans AI-drafted replies, but you are responsible for what posts. Read every draft at first and follow the rules in our management guide.
Q: How do I find my location ID? A: List accounts with the Account Management API, then list locations with the Business Information API (see the curl commands above). Combine them as accounts/ACCOUNT_ID/locations/LOCATION_ID.
Q: Can I use a different model provider? A: Yes. Replace the URL, auth header and model name in draft_reply(). Keep the HOLD rule and the length check.
Q: Can I delay replies so I can handle some personally? A: Yes. Each review has a createTime in RFC 3339 format. Parse it, compare it with the current time, and skip reviews newer than your chosen delay.
Q: Should I also automate asking for reviews? A: Only with one neutral request sent to every customer. Google prohibits selectively soliciting positive reviews. See our review request automation guide.

Sources

Prefer not to maintain scripts? See our AI agents service or contact DataLatte. Our Business Profile checklist covers the profile side.

Free for local businesses

Want this applied to your business?

I'll review your Google presence, local SEO, and ad accounts — and send you a specific action plan within 48 hours. No pitch, no pressure.

Want hands-on help?

See how DataLatte handles AI Agents & Automation for local businesses.

Learn more
Nataliia — local marketing expert
Nataliia

Local marketing strategist with 10+ years at global agencies — OMD, Dentsu, GroupM, and BBDO. Now helping small businesses get the same data-driven edge. Based in Europe, working with clients in the US, UK, Australia, and beyond.

About Nataliia

Want this applied to your business?

Let's review your current marketing setup together — free, no obligations.

Get Your Free Marketing Audit