# Google Business Profile

Publish updates, events and offers with call-to-action buttons to Google Business Profile locations on Search and Maps.


## Overview

The `google_business` platform creates local posts on Google Business Profile locations (shown on Google Search and Maps). You can post:

- **Updates** (`STANDARD`): text with an optional photo
- **Events** (`EVENT`): title, start and end time
- **Offers** (`OFFER`): title, dates, coupon code, redeem link and terms
- An optional call-to-action button (Book, Order online, Buy, Learn more, Sign up, Call now)

## Connect the account

In **[Integrations](https://social-publish-kappa.vercel.app/dashboard/integrations)**, choose **Google Business Profile** and sign in with a Google account that manages the business. **Every location** that account can manage becomes its own account in Relaycast (shown as the location name and city).

### App setup (self-hosted)

Google Business Profile uses the same Google Cloud project and OAuth client as [YouTube](/docs/platforms/youtube) (`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`).

1. Enable **My Business Account Management API**, **My Business Business Information API** and **Google My Business API** in the project.
2. Request Business Profile API access from Google. Until Google approves it, every one of these APIs has a quota of 0 requests per minute and connecting or posting fails.
3. Add the redirect URI `https://social-publish-kappa.vercel.app/api/integrations/google_business/callback` to the OAuth client (override with `GOOGLE_BUSINESS_REDIRECT_URI`).

Scope requested: `https://www.googleapis.com/auth/business.manage`. Access tokens are refreshed automatically.

## Post types & media rules

| Rule | Limit |
| --- | --- |
| Text (`content`) | 1,500 characters |
| Media | At most 1 photo; videos are not supported through the API |
| Event / offer title | 58 characters; required for `EVENT` and `OFFER` |
| Event / offer dates | `eventStart` and `eventEnd` required for `EVENT` and `OFFER`, format `YYYY-MM-DDTHH:mm`; end must be after start |
| Coupon code | 58 characters (offers) |
| Terms | 5,000 characters (offers) |
| Button link | `ctaUrl` required for every button except `CALL` |

`eventStart` and `eventEnd` are wall-clock times in the **business's own time zone**, not UTC. The `CALL` button uses the phone number on the Business Profile.

## Options

| Option | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `topicType` | `STANDARD` \\| `EVENT` \\| `OFFER` | No | `"STANDARD"` | Post type |
| `ctaType` | `none` \\| `BOOK` \\| `ORDER` \\| `SHOP` \\| `LEARN_MORE` \\| `SIGN_UP` \\| `CALL` | No | `"none"` | Button |
| `ctaUrl` | string (URL) | No |  | Button link. Required for every button except Call now (which uses the business phone number). Only when `ctaType` is `BOOK` or `ORDER` or `SHOP` or `LEARN_MORE` or `SIGN_UP`. |
| `eventTitle` | string | No |  | Event / offer title Only when `topicType` is `EVENT` or `OFFER`. |
| `eventStart` | string | No |  | Starts. YYYY-MM-DDTHH:mm in the business's local time. Only when `topicType` is `EVENT` or `OFFER`. |
| `eventEnd` | string | No |  | Ends. YYYY-MM-DDTHH:mm in the business's local time. Only when `topicType` is `EVENT` or `OFFER`. |
| `couponCode` | string | No |  | Coupon code Only when `topicType` is `OFFER`. |
| `redeemUrl` | string (URL) | No |  | Redeem online URL Only when `topicType` is `OFFER`. |
| `terms` | string | No |  | Terms and conditions Only when `topicType` is `OFFER`. |
## Examples

### Offer with a button

```bash tab="cURL"
curl -X POST https://social-publish-api.onrender.com/v1/posts \
  -H "x-access-key: $RELAYCAST_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "content": "20% off all coffee beans this weekend. Show the code in store or use it online.",
    "media": [{ "url": "https://cdn.example.com/beans.jpg", "type": "image" }],
    "targets": [{
      "accountId": "0e1d2c3b-4a59-4687-b7c6-d5e4f3a2b1c0",
      "options": {
        "topicType": "OFFER",
        "eventTitle": "Weekend bean sale",
        "eventStart": "2026-11-14T08:00",
        "eventEnd": "2026-11-15T18:00",
        "couponCode": "BEANS20",
        "redeemUrl": "https://example.com/shop",
        "terms": "One use per customer.",
        "ctaType": "SHOP",
        "ctaUrl": "https://example.com/shop"
      }
    }],
    "scheduledFor": "2026-11-13T09:00:00Z"
  }'
```

```ts tab="Node.js"
const res = await fetch("https://social-publish-api.onrender.com/v1/posts", {
  method: "POST",
  headers: {
    "x-access-key": process.env.RELAYCAST_API_KEY!,
    "content-type": "application/json",
  },
  body: JSON.stringify({
    content: "20% off all coffee beans this weekend. Show the code in store or use it online.",
    media: [{ url: "https://cdn.example.com/beans.jpg", type: "image" }],
    targets: [
      {
        accountId: "0e1d2c3b-4a59-4687-b7c6-d5e4f3a2b1c0",
        options: {
          topicType: "OFFER",
          eventTitle: "Weekend bean sale",
          eventStart: "2026-11-14T08:00",
          eventEnd: "2026-11-15T18:00",
          couponCode: "BEANS20",
          redeemUrl: "https://example.com/shop",
          terms: "One use per customer.",
          ctaType: "SHOP",
          ctaUrl: "https://example.com/shop",
        },
      },
    ],
    scheduledFor: "2026-11-13T09:00:00Z",
  }),
});
const { data } = await res.json();
```

```python tab="Python"
import os, requests

res = requests.post(
    "https://social-publish-api.onrender.com/v1/posts",
    headers={"x-access-key": os.environ["RELAYCAST_API_KEY"]},
    json={
        "content": "20% off all coffee beans this weekend. Show the code in store or use it online.",
        "media": [{"url": "https://cdn.example.com/beans.jpg", "type": "image"}],
        "targets": [{
            "accountId": "0e1d2c3b-4a59-4687-b7c6-d5e4f3a2b1c0",
            "options": {
                "topicType": "OFFER",
                "eventTitle": "Weekend bean sale",
                "eventStart": "2026-11-14T08:00",
                "eventEnd": "2026-11-15T18:00",
                "couponCode": "BEANS20",
                "redeemUrl": "https://example.com/shop",
                "terms": "One use per customer.",
                "ctaType": "SHOP",
                "ctaUrl": "https://example.com/shop",
            },
        }],
        "scheduledFor": "2026-11-13T09:00:00Z",
    },
)
data = res.json()["data"]
```

### Update with a Call now button

```json title="Request body"
{
  "content": "We're open late this Friday until 11pm.",
  "targets": [{
    "accountId": "0e1d2c3b-4a59-4687-b7c6-d5e4f3a2b1c0",
    "options": { "topicType": "STANDARD", "ctaType": "CALL" }
  }],
  "publishNow": true
}
```

## Analytics & comments

Post analytics are **not available**: Google retired the only per-post insights API in 2023, and the remaining Performance API only reports location-level metrics. Reviews, Q&A and messaging are not supported.

## Limitations & common errors

| Error | Cause |
| --- | --- |
| `Google Business posts allow 1500 characters` | `content` is too long. |
| `Google Business posts created through the API support photos only` | A video was attached. |
| `Google Business posts allow at most one photo` | More than one image. |
| `A URL is required for this call-to-action button` | `ctaType` other than `none`/`CALL` without `ctaUrl`. |
| `EVENT posts need a title` / `a start date/time` / `an end date/time` | Missing event or offer fields. |
| `Use YYYY-MM-DDTHH:mm (business local time)` | Wrong date format (no seconds, no time zone). |
| `End must be after start` | `eventEnd` is before `eventStart`. |
| Connect or post fails: API access not approved | Business Profile API access hasn't been granted, or the APIs aren't enabled (quota 0). |
| Connect fails: no locations found | The Google account doesn't manage any Business Profile locations. |

Posts are created with language `en`.
