Developer documentation
Read a business's Google Business Profile performance — profile views, calls, website clicks, directions, bookings and messages — plus local rankings, reviews and citations, into your own website. One API key, JSON out. No OAuth, no Google API keys to manage.
The only way in: create an API key in your
account, send it as a bearer token, and call https://www.gmbranking.com/api/v1/….
Keys are read-only and can be limited to a single location.
Overview
GMB Ranking pulls the numbers from Google and exposes them as JSON. Six metric series per location:
| Metric key | Label | Google source |
|---|---|---|
views | Profile views | Search + Maps impressions (desktop & mobile, summed) |
calls | Calls | CALL_CLICKS |
website | Website clicks | WEBSITE_CLICKS |
directions | Directions | BUSINESS_DIRECTION_REQUESTS |
bookings | Bookings | BUSINESS_BOOKINGS |
messages | Messages | BUSINESS_CONVERSATIONS |
Every series is a monthly array, oldest first, aligned to the same labels
array — everything you need to draw cards and charts.
Quick start
- Create an API key. In GMB Ranking open your profile menu → Developer API → Create key. Copy it immediately: it is shown once. (Plans that include API access only.)
-
List your locations.
curl https://www.gmbranking.com/api/v1/businesses \ -H "Authorization: Bearer YOUR_KEY" \ -H "Accept: application/json" -
Fetch one location's performance.
curl "https://www.gmbranking.com/api/v1/businesses/42/performance?months=6" \ -H "Authorization: Bearer YOUR_KEY" \ -H "Accept: application/json"
Create a key
Keys are self-service:
https://www.gmbranking.com/account/api
Give it a name you will recognise, copy the value straight away, and revoke it whenever you want. You can hold up to 10, each with its own name and scope, and every key carries a single read ability: it can read data and change nothing.
Limit a key to one location
Leave Which locations? on All locations and the key reads the whole account. Pick a single location instead and the key can read only that one — useful when handing a client's developer access without exposing the rest:
GET /api/v1/businessesreturns just that location- Any other id answers
403 {"message": "This API key is limited to one location."} GET /api/v1/mereports it as"key_scope": 42(null= account-wide)
Authentication
Every request needs the key as a bearer token:
Authorization: Bearer YOUR_KEY
Accept: application/json
No OAuth, no signing, no session — one header on an HTTPS GET.
Endpoints
| Endpoint | Returns |
|---|---|
GET/api/v1/me | Account, plan, location count, and the key's scope |
GET/api/v1/businesses | Every location: address, phone, rating, categories, 30-day view/call/click counts |
GET/api/v1/businesses/{id} | One location in full, plus saved reports (checklist, AI visibility, backlink summary) |
GET/api/v1/businesses/{id}/performance | Monthly series, ?months=1|3|6|12 |
GET/api/v1/businesses/{id}/rankings | Latest grid scan and history per keyword. Add ?include_grid=1 for the full grid |
GET/api/v1/businesses/{id}/reviews | Stored Google reviews plus external ones |
GET/api/v1/businesses/{id}/citations | Citation / NAP status per directory |
All endpoints are HTTPS GET, return JSON, and only ever expose the locations on your own account.
Performance
GET/api/v1/businesses/{id}/performance
Pick the period — 1M / 3M / 6M / 1Y
Pass the same labels the Performance page shows, or the number of months. Both mean the same thing; pick whichever suits your code.
| Range | Query | Months |
|---|---|---|
| 1M | ?range=1M or ?months=1 | 1 |
| 3M | ?range=3M or ?months=3 | 3 |
| 6M | ?range=6M or ?months=6 | 6 (default) |
| 1Y | ?range=1Y or ?months=12 | 12 |
curl "https://www.gmbranking.com/api/v1/businesses/42/performance?range=1Y" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Accept: application/json"
The response echoes what you asked for in range (and months), so a
dashboard can render the selected 1M / 3M / 6M / 1Y button straight from it.
An unknown value is refused rather than quietly switched to 6 months:
HTTP/1.1 422 Unprocessable Content
{
"message": "Unknown range \"2Y\". Use one of: 1M, 3M, 6M, 1Y (or months=1, 3, 6, 12).",
"code": "invalid_range",
"available_ranges": { "1M": 1, "3M": 3, "6M": 6, "1Y": 12 }
}
{
"business": { "id": 42, "name": "Acme Plumbing", "google_connected": true },
"range": "6M",
"months": 6,
"available_ranges": { "1M": 1, "3M": 3, "6M": 6, "1Y": 12 },
"labels": ["May", "Jun", "Jul", "Aug", "Sep", "Oct"],
"metrics": {
"views": { "label": "Profile views", "data": [263, 296, 241, 259, 273, 208] },
"calls": { "label": "Calls", "data": [6, 1, 4, 4, 8, 2] },
"website": { "label": "Website clicks", "data": [28, 23, 14, 16, 14, 13] },
"directions": { "label": "Directions", "data": [23, 33, 79, 31, 55, 27] },
"bookings": { "label": "Bookings", "data": [0, 0, 0, 0, 0, 0] },
"messages": { "label": "Messages", "data": [0, 0, 0, 0, 0, 0] }
},
"error": null,
"generated_at": "2026-10-24T16:42:11+00:00"
}
200 with
metrics: {} and a reason in error
("Connect this business to a Google Business Profile to see performance data.").
Show that message instead of empty cards.
Rankings, reviews & citations
GET/api/v1/businesses/{id}/rankings?include_grid=1
{
"count": 1,
"data": [{
"keyword": "plumber near me",
"avg_position": 3.42,
"found_in": 41,
"grid_size": 7,
"radius_km": 4,
"scanned_at": "2026-10-20T11:04:00+00:00",
"history": [
{ "avg_position": 3.42, "found_in": 41, "scanned_at": "2026-10-20T11:04:00+00:00" }
],
"grid": [{ "lat": 51.5072, "lng": -0.1276, "position": 3, "top": [] }]
}]
}
avg_position is the average across the whole grid, so it carries a decimal —
Ø 1.5 means the business sits around position 1–2 across the scanned area.
GET/api/v1/businesses/{id}/reviews returns
rating, reviews_count, the stored data array and any
external reviews. GET
/api/v1/businesses/{id}/citations returns one row per directory with
status, the NAP it actually found, and when it was last checked.
Response fields
| Field | Type | Meaning |
|---|---|---|
business.id | integer | Location id inside GMB Ranking |
business.name | string | Business name as published on Google |
business.google_connected | boolean | false = no Google location linked, so metrics stay empty |
range | string | The window you asked for, as the UI labels it: 1M, 3M, 6M or 1Y |
months | integer | The same window in months (1, 3, 6, 12) |
available_ranges | object | Every range you may ask for — handy for building the buttons |
labels | string[] | Month labels, oldest first — one per value in every series |
metrics.{key}.label | string | Display name |
metrics.{key}.data | integer[] | One value per label; 0 is a real zero |
error | string|null | null on success, otherwise why metrics are unavailable |
generated_at | ISO 8601 | When the document was assembled |
key_scope | integer|null | Location id the key is limited to (/me only) |
Errors
| Status | When | What to do |
|---|---|---|
401 unauthenticated | No key, or the key was revoked | Check the Authorization: Bearer header |
403 plan_required | Key valid, subscription ended | Renew — the same key works again, no new key needed |
403 limited to one location | Key scoped to a different location | Create a key for that location, or one for all locations |
404 | Unknown id, or not on your account | List locations first |
422 invalid_range | Unknown range/months value | Use 1M, 3M, 6M, 1Y |
422 | Google call failed for this location | Read message; retry later |
429 | More than 120 requests a minute for this key | Cache and back off |
Code examples
cURL
curl "https://www.gmbranking.com/api/v1/businesses/42/performance?months=6" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Accept: application/json"
PHP
<?php
$key = 'YOUR_KEY';
$url = 'https://www.gmbranking.com/api/v1/businesses/42/performance?months=6';
$context = stream_context_create(['http' => [
'header' => "Authorization: Bearer {$key}\r\nAccept: application/json\r\n",
'timeout' => 15,
]]);
$data = json_decode(file_get_contents($url, false, $context), true);
foreach ($data['metrics'] as $metric) {
echo $metric['label'] . ': ' . array_sum($metric['data']) . PHP_EOL;
}
Node.js (server, Node 18+)
const KEY = process.env.GMB_API_KEY;
const BASE = 'https://www.gmbranking.com';
const res = await fetch(`${BASE}/api/v1/businesses`, {
headers: { Authorization: `Bearer ${KEY}`, Accept: 'application/json' },
});
if (!res.ok) {
const err = await res.json();
throw new Error(`${res.status}: ${err.message}`);
}
const { data } = await res.json();
for (const business of data) {
const perf = await fetch(
`${BASE}/api/v1/businesses/${business.id}/performance?months=6`,
{ headers: { Authorization: `Bearer ${KEY}`, Accept: 'application/json' } },
).then((r) => r.json());
console.log(business.name, perf.metrics?.views?.data ?? perf.error);
}
Python (server)
# pip install requests
import os
import requests
KEY = os.environ["GMB_API_KEY"]
BASE = "https://www.gmbranking.com"
HEADERS = {"Authorization": f"Bearer {KEY}", "Accept": "application/json"}
res = requests.get(f"{BASE}/api/v1/businesses", headers=HEADERS, timeout=15)
if res.status_code == 403 and res.json().get("code") == "plan_required":
raise SystemExit("GMB Ranking plan has ended — renew to restore data")
res.raise_for_status()
for business in res.json()["data"]:
perf = requests.get(
f"{BASE}/api/v1/businesses/{business['id']}/performance",
params={"months": 6}, headers=HEADERS, timeout=15,
).json()
views = perf.get("metrics", {}).get("views", {}).get("data")
print(business["name"], views or perf.get("error"))
Any other language — three ingredients
No SDK, no OAuth. Whatever the language, it is the same three things:
| 1 · URL | https://www.gmbranking.com/api/v1/… |
|---|---|
| 2 · Header | Authorization: Bearer YOUR_KEY and Accept: application/json |
| 3 · Body | JSON in, JSON out — UTF-8 |
That covers PHP, Node.js, Python, Go, Ruby, Java, C#/.NET, Rust, Kotlin, Swift — anything that
can send an HTTPS GET with a header. For typed clients, feed
openapi.json to
openapi-generator, Postman, Insomnia or Swagger Codegen.
Drawing a chart
Chart.js in a dozen lines — the series map straight onto it:
new Chart(document.getElementById('views'), {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: data.metrics.views.label,
data: data.metrics.views.data,
borderColor: '#223a86',
backgroundColor: 'rgba(34,58,134,.15)',
fill: true,
tension: .35
}]
},
options: { plugins: { legend: { display: false } } }
});
Troubleshooting
| What you see | Why | Fix |
|---|---|---|
401 | No key, wrong key, or revoked | Check Authorization: Bearer KEY |
403 plan_required | Key fine, subscription ended | Renew — the same key works again |
403 limited to one location | Key scoped elsewhere | Create a key for this location |
200 but empty metrics | Location not imported from Google | Use Import from Google on the Performance page once |
429 | Over 120 requests/minute | Cache the response; data changes a few times a day |
| Last month looks low | Google is 2–3 days behind and the month is partial | Compare whole months |
| Same numbers for hours | Responses cached 30 minutes server-side | Expected — do not poll faster |
When a plan ends
The API comes with the paid plans that include API access (they are listed on the pricing page; plan names below are examples). The free trial does not include it. If the subscription lapses, every call keeps authenticating but is refused:
HTTP/1.1 403 Forbidden
{
"message": "This account's GMB Ranking plan does not include API access. A Pro or Agency subscription is required; existing keys work again as soon as the plan is renewed.",
"code": "plan_required",
"status": "plan_required",
"plan": "free",
"required_plans": ["pro", "agency"],
"upgrade_url": "https://www.gmbranking.com/billing",
"docs_url": "https://developers.gmbranking.com/#plan"
}
required_plans and the plan names inside message come from the
account's plan configuration, so they match whatever the plans are called. Branch on
code, never on the wording.
Caching & data freshness
| Layer | Delay |
|---|---|
| Google's own reporting | 2–3 days — nothing can make it faster |
| GMB Ranking cache | Up to 30 minutes per location and window |
| Rate limit | 120 requests per minute per key |
Label the numbers “updated with a 2–3 day delay” in your UI, and cache on your side — polling faster than 30 minutes cannot show new data.
Security
- Keys are read-only and hashed at rest — the full value is shown once.
- A key reads only its own account's locations, and only the one location if you scoped it.
- Revoking a key stops it instantly; every key shows last used so unused ones are obvious.
- All traffic is HTTPS. Keys must stay server-side — never in a browser bundle or a public repo.
- Ten keys per account; 120 requests a minute per key.
Changelog
| Date | Change |
|---|---|
| 2026-10-24 | Performance accepts ?range=1M|3M|6M|1Y (same as the app's buttons); unknown values return 422 |
| 2026-10-24 | Docs streamlined to a single integration path (API keys). Read-only keys can be limited to one location. |
| 2026-10-24 | API access limited to paid plans; lapsed plans return 403 with code: "plan_required" |
| 2026-10-24 | Runnable Node.js and Python examples, plus an “any language” recipe |
| 2026-10-24 | Self-service API keys and read-only /api/v1/* |