---
name: gbp-change-website
description: Change the website URL on a Google Business Profile listing through the Business Information API, safely - validate first, save only websiteUri, read it back, and handle Google's edit review. Use when an app already has an OAuth connection with the business.manage scope and must update a listing's website.
---

# Change a Google Business Profile website (API)

Updates only the **website** field of one listing. Live-tested: a real save on an owner-managed listing was accepted, the read-back showed the new URL immediately, and it went live on the public listing. Changing it back four minutes later went into a slower review.

## Prerequisites

- OAuth access token with `https://www.googleapis.com/auth/business.manage` (the same scope used to read reviews and locations; no extra consent needed).
- The location's **Business Information** name: `locations/{locationId}`. The reviews API (v4) uses `accounts/{accountId}/locations/{locationId}`; strip the account part for this call.
- Your Google Cloud project has Business Profile API access.

## The calls

1. **Read current value**
   `GET https://mybusinessbusinessinformation.googleapis.com/v1/locations/{id}?readMask=websiteUri,metadata`
   Keep `websiteUri` (may be missing) and `metadata.hasPendingEdits`.
2. **Validate only** (nothing saved)
   `PATCH https://mybusinessbusinessinformation.googleapis.com/v1/locations/{id}?updateMask=websiteUri&validateOnly=true`
   body `{"websiteUri":"https://www.example.com/"}`. An empty response means Google would accept it; HTTP 400 means it would not.
3. **Save**: the same PATCH without `validateOnly`.
4. **Read back** with call 1. Report `previous`, `current`, and `pendingReview = metadata.hasPendingEdits`.

```ts
const BASE = "https://mybusinessbusinessinformation.googleapis.com/v1";
async function g(token: string, url: string, init: RequestInit = {}) {
  const r = await fetch(url, { ...init, cache: "no-store", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" } });
  if (!r.ok) throw new Error(`Google API error: ${r.status} - ${await r.text()}`);
  const text = await r.text(); // validateOnly may answer with an empty body
  return text ? JSON.parse(text) : {};
}
const read = async (t: string, loc: string) => {
  const d = await g(t, `${BASE}/${loc}?readMask=websiteUri,metadata`);
  return { websiteUri: d.websiteUri ?? null, pending: d.metadata?.hasPendingEdits === true };
};
export async function changeWebsite(token: string, loc: string /* locations/{id} */, url: string, checkOnly = false) {
  const before = await read(token, loc);
  const patch = (validate: boolean) => g(token, `${BASE}/${loc}?updateMask=websiteUri${validate ? "&validateOnly=true" : ""}`,
    { method: "PATCH", body: JSON.stringify({ websiteUri: url }) });
  await patch(true);
  if (checkOnly) return { saved: false, previous: before.websiteUri, requested: url };
  await patch(false);
  const after = await read(token, loc);
  return { saved: true, previous: before.websiteUri, current: after.websiteUri, requested: url, pendingReview: after.pending };
}
```

## URL rule (check before calling Google)

Accept only `https:`/`http:`, a real hostname with a dot, no `user:pass@`, at most 2048 characters. If the user typed `example.com`, prefix `https://`. Normalize with `new URL(...).toString()`.

## Safety rules

- **Who can edit is your decision, not Google's.** The token can edit the public listing. Gate the action on your side (for example owner/admin only) and re-check on the server, not just by hiding a button.
- **Two-step UI.** Check with Google, show Now vs New, then Save. Require an explicit confirm flag in the request.
- **Audit every save.** Log who made the change, the location, the previous, requested and current values, and pendingReview.
- **Read back after saving.** Trust what Google returns, not what you sent. Google may normalize the URL, for example by adding a trailing slash.
- Map Google HTTP 400 to "Google did not accept that website" and 401 to "reconnect". Treat 403 as missing API access or permission on that listing.

## Google's review (what users will see)

- Every edit is moderated. Google's help says: "Edits usually take up to 10 minutes to review, but sometimes it can take up to 30 days."
- The API read-back can show the new URL while the public listing still shows the old one. `hasPendingEdits: true` means it's under review. The owner may see "Your edit is taking a little longer to review. It may take up to 7 days to be published."
- Don't edit the same field again while it's pending; reports say that restarts the review. Changing to a different domain or subdomain appears to take longer, probably because Google cross-checks the new site against the business's other details. That's an inference, not documented.

## Tested

- **Live:** one real save on an owner-managed listing that changed from a subdomain to the main domain. The check-only call passed, the save was accepted, and the read-back returned the new URL with `hasPendingEdits: true`. The change then published: Google's dashboard showed it as "Current". Reverting the same field four minutes later was accepted but held: "Your edit is taking a little longer to review. It may take up to 7 days to be published." When testing, change once and wait for it to publish before reverting.
- **Offline:** 10/10 URL-rule checks; server permission gate (anonymous and forged sessions refused, unconfirmed and bad-URL requests rejected).
- **Not tested:** a Google 400 rejection on a live listing, or a listing without Voice of Merchant.
