Rank tracking
Where do your keywords rank in Google? Track any domain - hosted with us or not - in any country, language and device, with the full position history.
Rank tracking: plan and usage
GET/api/v1/rankScope rank:read
Whether rank tracking is on this account, the plan, how many keywords it covers and how many are tracked, how often they are checked (weekly or daily), and the number of tracked domains. When it is not on the account, plans_url is the page to buy it.
Example
curl -s "https://app.pbn.ltd/api/v1/rank" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/rank", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/rank", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
200
{
"data": {
"active": true,
"plan": "Pro",
"paid_until": "2026-10-31",
"keywords": {
"used": 120,
"limit": 250
},
"cadence": "daily",
"domains": 3,
"reduced_to_weekly_this_month": false,
"plans_url": "https://app.pbn.ltd/rank/plans/"
}
}
Errors: rate_limited, scope_missing, unauthorized
Rank tracking plans and prices
GET/api/v1/rank/plansScope rank:read
The plans and the extra-keyword pack with their live prices (US dollars a month, before VAT). Buying happens on plans_url in the browser - the payment needs your own approval at the payment provider and the Terms tick, so the API never charges.
Example
curl -s "https://app.pbn.ltd/api/v1/rank/plans" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/rank/plans", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/rank/plans", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
200
{
"data": {
"plans": [
{
"key": "rank100",
"name": "Starter",
"price_usd": "5.00",
"keywords": 100,
"cadence": "weekly",
"summary": ""
}
],
"pack": {
"key": "rank50p",
"name": "+50 keywords",
"price_usd": "2.00",
"keywords": 50,
"max_per_account": 40
},
"months_offered": [
1,
3,
6,
12
],
"can_buy": true,
"plans_url": "https://app.pbn.ltd/rank/plans/"
}
}
Errors: rate_limited, scope_missing, unauthorized
List tracked domains
GET/api/v1/rank/domainsScope rank:read
Every domain you track, with its keyword count, average position and how many keywords are in the top 10 and top 3.
Example
curl -s "https://app.pbn.ltd/api/v1/rank/domains" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/rank/domains", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/rank/domains", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
200
{
"data": [
{
"id": 55,
"domain": "example.com",
"label": "",
"hosted_site_id": 12345,
"keywords": 40,
"avg_position": 14.2,
"prev_avg_position": 15.8,
"top10": 11,
"top3": 4,
"last_checked_at": "2026-09-24T06:00:00Z",
"url": "https://app.pbn.ltd/rank/domain/55/"
}
]
}
Errors: rate_limited, scope_missing, unauthorized
One tracked domain with its keywords
GET/api/v1/rank/domains/{domain_id}Scope rank:read
The domain, each Google version it is tracked in (country, language, device), and every keyword in each with its current position (null = not in the first 100 results).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
domain_id | path | integer | yes | The tracked domain id. |
Example
curl -s "https://app.pbn.ltd/api/v1/rank/domains/domain_id" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/rank/domains/domain_id", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/rank/domains/domain_id", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
200
{
"data": {
"id": 55,
"domain": "example.com",
"label": "",
"hosted_site_id": 12345,
"keywords": 40,
"avg_position": 14.2,
"prev_avg_position": 15.8,
"top10": 11,
"top3": 4,
"last_checked_at": "2026-09-24T06:00:00Z",
"url": "https://app.pbn.ltd/rank/domain/55/",
"groups": [
{
"id": 301,
"google": "Google United Kingdom \u00b7 English \u00b7 Mobile",
"location_code": 2826,
"location": "United Kingdom",
"country": "GB",
"language_code": "en",
"language": "English",
"device": "mobile",
"name": "",
"keywords": [
{
"id": 9001,
"keyword": "best widgets",
"domain_id": 55,
"group_id": 301,
"active": true,
"position": 7,
"previous_position": 9,
"change": 2,
"best_position": 5,
"worst_position": 18,
"ranking_url": "https://example.com/widgets/",
"checks": 30,
"last_checked_at": "2026-09-24T06:00:00Z"
}
]
}
]
}
}
Errors: not_found, rate_limited, scope_missing, unauthorized
One keyword and its position history
GET/api/v1/rank/keywords/{keyword_id}Scope rank:read
The keyword and every completed check in the period: the position (null = not in the first 100 results) and the page of yours that ranked.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
keyword_id | path | integer | yes | The keyword id. |
days | query | integer | no | How many days of history (1-400). Default: 90. |
Example
curl -s "https://app.pbn.ltd/api/v1/rank/keywords/keyword_id" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/rank/keywords/keyword_id", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/rank/keywords/keyword_id", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
200
{
"data": {
"id": 9001,
"keyword": "best widgets",
"domain_id": 55,
"group_id": 301,
"active": true,
"position": 7,
"previous_position": 9,
"change": 2,
"best_position": 5,
"worst_position": 18,
"ranking_url": "https://example.com/widgets/",
"checks": 30,
"last_checked_at": "2026-09-24T06:00:00Z",
"google": "Google United Kingdom \u00b7 English \u00b7 Mobile",
"history": [
{
"checked_at": "2026-09-24T06:00:00Z",
"position": 7,
"ranking_url": "https://example.com/widgets/"
}
]
}
}
Errors: not_found, rate_limited, scope_missing, unauthorized
Find a Google location
GET/api/v1/rank/locationsScope rank:read
The location codes to use for a Google version (POST /rank/domains/{domain_id}/groups). Up to 40 matches.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
q | query | string | no | Part of a country, region or city name. Empty = the countries. |
Example
curl -s "https://app.pbn.ltd/api/v1/rank/locations" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/rank/locations", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/rank/locations", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
200
{
"data": [
{
"code": 2826,
"name": "United Kingdom",
"type": "Country"
}
]
}
Errors: rate_limited, scope_missing, unauthorized
Search languages
GET/api/v1/rank/languagesScope rank:read
The language codes to use for a Google version.
Example
curl -s "https://app.pbn.ltd/api/v1/rank/languages" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/rank/languages", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/rank/languages", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
200
{
"data": [
{
"code": "en",
"name": "English"
}
]
}
Errors: rate_limited, scope_missing, unauthorized
Track a domain
POST/api/v1/rank/domainsScope rank:write
Starts tracking a domain (hosted with us or anywhere else). Then add a Google version and keywords. Adding a domain you track already returns it.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
domain | body | string | yes | The domain, e.g. example.com (a URL or www. is accepted). |
label | body | string | no | An optional label of your own. |
Example
curl -s -X POST "https://app.pbn.ltd/api/v1/rank/domains" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/rank/domains", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/rank/domains", {
method: "POST",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
201
{
"data": {
"id": 55,
"domain": "example.com",
"label": "",
"hosted_site_id": 12345,
"keywords": 40,
"avg_position": 14.2,
"prev_avg_position": 15.8,
"top10": 11,
"top3": 4,
"last_checked_at": "2026-09-24T06:00:00Z",
"url": "https://app.pbn.ltd/rank/domain/55/"
}
}
Errors: payment_required, rate_limited, scope_missing, unauthorized, validation_failed
Stop tracking a domain
DELETE/api/v1/rank/domains/{domain_id}Scope rank:write · destructive
Stops tracking the domain and all its keywords. The history is kept, but re-adding the domain does not restart its keywords - they have to be added again. Needs "confirm": true.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
domain_id | path | integer | yes | The tracked domain id. |
confirm | body | boolean | yes | Must be true: this cannot be undone from the API. |
Example
curl -s -X DELETE "https://app.pbn.ltd/api/v1/rank/domains/domain_id" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.delete("https://app.pbn.ltd/api/v1/rank/domains/domain_id", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/rank/domains/domain_id", {
method: "DELETE",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
200
{
"data": {
"removed": true,
"id": 55
}
}
Errors: not_found, rate_limited, scope_missing, unauthorized, validation_failed
Add a Google version to a domain
POST/api/v1/rank/domains/{domain_id}/groupsScope rank:write
Which Google to check the keywords in: country or city, language and device. Up to 25 per domain. Adding one that exists returns it.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
domain_id | path | integer | yes | The tracked domain id. |
location_code | body | integer | yes | From GET /rank/locations (2826 = United Kingdom, 2840 = United States). |
language_code | body | string | yes | From GET /rank/languages. |
device | body | string (one of: desktop, mobile) | yes | Desktop or mobile results. |
name | body | string | no | An optional name of your own. |
Example
curl -s -X POST "https://app.pbn.ltd/api/v1/rank/domains/domain_id/groups" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/rank/domains/domain_id/groups", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/rank/domains/domain_id/groups", {
method: "POST",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
201
{
"data": {
"id": 301,
"google": "Google United Kingdom \u00b7 English \u00b7 Mobile",
"location_code": 2826,
"location": "United Kingdom",
"country": "GB",
"language_code": "en",
"language": "English",
"device": "mobile",
"name": ""
}
}
Errors: not_found, rate_limited, scope_missing, unauthorized, validation_failed
Add keywords
POST/api/v1/rank/groups/{group_id}/keywordsScope rank:write
Adds keywords to a Google version with the panel's own rule: what fits inside your plan is added, the rest is listed in not_added (add a keyword pack or move up a plan). The first check runs within the hour. Keywords you track already are skipped.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
group_id | path | integer | yes | The Google version (keyword group) id, from GET /rank/domains/{domain_id}. |
keywords | body | array | yes | The keywords (or one string, one per line or comma separated). |
Example
curl -s -X POST "https://app.pbn.ltd/api/v1/rank/groups/group_id/keywords" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/rank/groups/group_id/keywords", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/rank/groups/group_id/keywords", {
method: "POST",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
201
{
"data": {
"added": [
{
"id": 9001,
"keyword": "best widgets",
"domain_id": 55,
"group_id": 301,
"active": true,
"position": 7,
"previous_position": 9,
"change": 2,
"best_position": 5,
"worst_position": 18,
"ranking_url": "https://example.com/widgets/",
"checks": 30,
"last_checked_at": "2026-09-24T06:00:00Z"
}
],
"not_added": {
"count": 0,
"keywords": [],
"limit": 250,
"plans_url": "https://app.pbn.ltd/rank/plans/"
}
}
}
Errors: not_found, rate_limited, scope_missing, unauthorized, validation_failed
Remove a keyword
DELETE/api/v1/rank/keywords/{keyword_id}Scope rank:write · destructive
Removes the keyword AND its position history (the same as the Remove button). Needs "confirm": true.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
keyword_id | path | integer | yes | The keyword id. |
confirm | body | boolean | yes | Must be true: this cannot be undone from the API. |
Example
curl -s -X DELETE "https://app.pbn.ltd/api/v1/rank/keywords/keyword_id" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.delete("https://app.pbn.ltd/api/v1/rank/keywords/keyword_id", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/rank/keywords/keyword_id", {
method: "DELETE",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
200
{
"data": {
"removed": true,
"id": 9001
}
}
Errors: not_found, rate_limited, scope_missing, unauthorized, validation_failed
Check a keyword now
POST/api/v1/rank/keywords/{keyword_id}/checkScope rank:write
Asks for an extra check of this keyword now (results usually within the hour). The same bounds as the button: once per keyword in 24 hours and a daily number per account, and not while checks are paused. Scheduled checks carry on regardless.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
keyword_id | path | integer | yes | The keyword id. |
Example
curl -s -X POST "https://app.pbn.ltd/api/v1/rank/keywords/keyword_id/check" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/rank/keywords/keyword_id/check", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/rank/keywords/keyword_id/check", {
method: "POST",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
202
{
"data": {
"queued": true,
"id": 9001
}
}
Errors: not_found, rate_limited, scope_missing, unauthorized, unavailable