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

```bash
curl -s "https://app.pbn.ltd/api/v1/rank" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "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 /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

```bash
curl -s "https://app.pbn.ltd/api/v1/rank/plans" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "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 /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

```bash
curl -s "https://app.pbn.ltd/api/v1/rank/domains" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "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 /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

```bash
curl -s "https://app.pbn.ltd/api/v1/rank/domains/domain_id" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "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 /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

```bash
curl -s "https://app.pbn.ltd/api/v1/rank/keywords/keyword_id" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "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 /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

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `q` | query | string | no | Part of a country, region or city name. Empty = the countries. |

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/rank/locations" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "data": [
    {
      "code": 2826,
      "name": "United Kingdom",
      "type": "Country"
    }
  ]
}
```

Errors: `rate_limited`, `scope_missing`, `unauthorized`

## Search languages

`GET /rank/languages` · scope `rank:read`

The language codes to use for a Google version.

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/rank/languages" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "data": [
    {
      "code": "en",
      "name": "English"
    }
  ]
}
```

Errors: `rate_limited`, `scope_missing`, `unauthorized`

## Track a domain

`POST /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

| 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

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/rank/domains" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "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 /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

```bash
curl -s -X DELETE "https://app.pbn.ltd/api/v1/rank/domains/domain_id" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "data": {
    "removed": true,
    "id": 55
  }
}
```

Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed`

## Add a Google version to a domain

`POST /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

| 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

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/rank/domains/domain_id/groups" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "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 /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

| 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

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/rank/groups/group_id/keywords" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "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 /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

```bash
curl -s -X DELETE "https://app.pbn.ltd/api/v1/rank/keywords/keyword_id" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "data": {
    "removed": true,
    "id": 9001
  }
}
```

Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed`

## Check a keyword now

`POST /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

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `keyword_id` | path | integer | yes | The keyword id. |

### Example

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/rank/keywords/keyword_id/check" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "data": {
    "queued": true,
    "id": 9001
  }
}
```

Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `unavailable`
