PBN.LTD API docs
View as Markdown

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/rank

Scope 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/plans

Scope 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/domains

Scope 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

NameInTypeRequiredDescription
domain_idpathintegeryesThe 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

NameInTypeRequiredDescription
keyword_idpathintegeryesThe keyword id.
daysqueryintegernoHow 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/locations

Scope rank:read

The location codes to use for a Google version (POST /rank/domains/{domain_id}/groups). Up to 40 matches.

Parameters

NameInTypeRequiredDescription
qquerystringnoPart 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/languages

Scope 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/domains

Scope 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

NameInTypeRequiredDescription
domainbodystringyesThe domain, e.g. example.com (a URL or www. is accepted).
labelbodystringnoAn 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

NameInTypeRequiredDescription
domain_idpathintegeryesThe tracked domain id.
confirmbodybooleanyesMust 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}/groups

Scope 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

NameInTypeRequiredDescription
domain_idpathintegeryesThe tracked domain id.
location_codebodyintegeryesFrom GET /rank/locations (2826 = United Kingdom, 2840 = United States).
language_codebodystringyesFrom GET /rank/languages.
devicebodystring (one of: desktop, mobile)yesDesktop or mobile results.
namebodystringnoAn 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}/keywords

Scope 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

NameInTypeRequiredDescription
group_idpathintegeryesThe Google version (keyword group) id, from GET /rank/domains/{domain_id}.
keywordsbodyarrayyesThe 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

NameInTypeRequiredDescription
keyword_idpathintegeryesThe keyword id.
confirmbodybooleanyesMust 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}/check

Scope 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

NameInTypeRequiredDescription
keyword_idpathintegeryesThe 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