Add a domain and track keywords
This tutorial walks the full path from an empty account to live ranking data:
create a domain, track a set of keywords on it, then read back the latest
rankings. Every step has a runnable curl example you can copy, paste your key
into, and run.
:::note What you'll need
An active Ranktracker account with API access enabled on your plan, an API
key (see the Quickstart and
Authentication guide), and enough keyword/data-row
quota to track what you add. All examples use the production base URL
https://api.ranktracker.com, and every endpoint lives under /v1.
:::
Throughout, send your key in the Authorization header with no Bearer
prefix, and note the JSON:API convention: request bodies use snake_case,
responses come back under data / attributes in camelCase. See
Core concepts for the full model.
Step 1 — Create a domain
A domain is the site whose rankings you want to track. Create one with
POST /v1/domains. The request body nests the attributes under a domain key:
| Field | Description |
|---|---|
host | The hostname to track, e.g. www.example.com. |
scheme | https or http. |
match_type | How a result is matched to your domain, e.g. any, domain, or url. |
project_name | A human-friendly label for the project. |
gmb_name | Google Business Profile name for local tracking (send an empty string if unused). |
colour | A hex colour used in the UI, e.g. #4f46e5. |
:::warning host, scheme, and match_type are immutable
Once a domain is created, host, scheme, and match_type are fixed — they
identify the target and cannot be changed later. Only editable fields (such as
project_name) can be updated with PATCH /v1/domains/{uuid}. If you got the
host or scheme wrong, delete the domain and create a new one.
:::
curl -X POST https://api.ranktracker.com/v1/domains \
-H "Authorization: tkn_usr_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"domain": {
"host": "www.example.com",
"scheme": "https",
"match_type": "any",
"project_name": "Example Site",
"gmb_name": "",
"colour": "#4f46e5"
}
}'
The same request in Python and Node.js:
import requests
resp = requests.post(
"https://api.ranktracker.com/v1/domains",
headers={"Authorization": "tkn_usr_your_api_key_here"},
json={
"domain": {
"host": "www.example.com",
"scheme": "https",
"match_type": "any",
"project_name": "Example Site",
"gmb_name": "",
"colour": "#4f46e5",
}
},
)
resp.raise_for_status()
domain = resp.json()["data"]
print(domain["id"], domain["attributes"]["projectName"])
const resp = await fetch("https://api.ranktracker.com/v1/domains", {
method: "POST",
headers: {
Authorization: "tkn_usr_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
domain: {
host: "www.example.com",
scheme: "https",
match_type: "any",
project_name: "Example Site",
gmb_name: "",
colour: "#4f46e5",
},
}),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const { data } = await resp.json();
console.log(data.id);
A 201 Created returns the new domain. Grab the id — it's the domain
UUID, and you'll pass it in the path for every keyword call that follows:
{
"data": {
"id": "3f2a9c8e-1d4b-4a7f-9e2c-8b1a2c3d4e5f",
"type": "domain",
"attributes": {
"domain": "example.com",
"scheme": "https",
"host": "www.example.com",
"matchType": "any",
"projectName": "Example Site",
"colour": "#4f46e5",
"keywordMonitor": true,
"competitors": [],
"createdAt": "2026-07-03T09:31:07Z",
"updatedAt": "2026-07-03T09:31:07Z"
}
}
}
If the attributes fail validation (for example a missing field or a malformed
host), you'll get a 422 Unprocessable Entity with a JSON:API error envelope.
See Errors & rate limits for the shape.
:::tip Save the UUID
Store data.id in a variable — the rest of this tutorial uses it as
{domain_uuid}. In a shell you can capture it with a JSON tool, e.g.
DOMAIN_UUID=$(... | jq -r '.data.id').
:::
Step 2 — Track keywords on the domain
Keywords are nested under a domain. POST /v1/domains/{domain_uuid}/keywords
takes three fields:
| Field | Description |
|---|---|
words | An array of search terms to track, e.g. ["rank tracker", "seo tools"]. |
search_engines | An array of engine configurations — each is a name + location + language + device combination. |
frequency | How often the keywords are checked, e.g. daily. |
Each entry in search_engines describes one engine + location + language +
device setup:
| Field | Description |
|---|---|
name | The search engine, e.g. google. |
location | The location to search from, e.g. United States. |
language | The results language, e.g. en. |
device | desktop or mobile. |
The cartesian product
The API tracks the cartesian product of words × search_engines. Every
word is paired with every search-engine configuration, and each pairing becomes
one tracked keyword (one row of ranking data). So this request:
- 2 words × 2 search-engine configs = 4 tracked keywords
Keep that multiplication in mind when you size a request — it's the number of keywords, and the data rows, that count against your plan.
curl -X POST https://api.ranktracker.com/v1/domains/3f2a9c8e-1d4b-4a7f-9e2c-8b1a2c3d4e5f/keywords \
-H "Authorization: tkn_usr_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"frequency": "daily",
"words": ["rank tracker", "seo tools"],
"search_engines": [
{
"name": "google",
"location": "United States",
"language": "en",
"device": "desktop"
},
{
"name": "google",
"location": "United States",
"language": "en",
"device": "mobile"
}
]
}'
import requests
domain_uuid = "3f2a9c8e-1d4b-4a7f-9e2c-8b1a2c3d4e5f"
engine = {"name": "google", "location": "United States", "language": "en"}
resp = requests.post(
f"https://api.ranktracker.com/v1/domains/{domain_uuid}/keywords",
headers={"Authorization": "tkn_usr_your_api_key_here"},
json={
"frequency": "daily",
"words": ["rank tracker", "seo tools"],
"search_engines": [
{**engine, "device": "desktop"},
{**engine, "device": "mobile"},
],
},
)
resp.raise_for_status()
print(f"tracked {len(resp.json()['data'])} keywords")
const domainUuid = "3f2a9c8e-1d4b-4a7f-9e2c-8b1a2c3d4e5f";
const engine = { name: "google", location: "United States", language: "en" };
const resp = await fetch(
`https://api.ranktracker.com/v1/domains/${domainUuid}/keywords`,
{
method: "POST",
headers: {
Authorization: "tkn_usr_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
frequency: "daily",
words: ["rank tracker", "seo tools"],
search_engines: [
{ ...engine, device: "desktop" },
{ ...engine, device: "mobile" },
],
}),
},
);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const { data } = await resp.json();
console.log(`tracked ${data.length} keywords`);
A 201 Created returns an array under data — one entry per tracked keyword
(the cartesian product). Rankings aren't in yet on a brand-new keyword, so
results.history starts empty and fills in as checks run:
{
"data": [
{
"id": "a1b2c3d4-0000-4a7f-9e2c-111122223333",
"type": "keyword",
"attributes": {
"word": "rank tracker",
"searchEngine": { "uuid": "…", "name": "google" },
"device": { "uuid": "…", "name": "desktop" },
"location": { "uuid": "…", "code": "US", "name": "United States" },
"language": { "uuid": "…", "code": "en", "name": "English" },
"results": { "summary": { "search_volume": null }, "history": [] },
"tags": [],
"updatedAt": "2026-07-03T09:32:10Z"
}
}
]
}
:::warning Over quota returns 402
If the request would push you past your plan's keyword or data-row
allowance, the API responds with 402 Payment Required and links nothing —
the whole request is rejected atomically, so you won't end up with a partial
batch. Check your remaining allowance first with
GET /v1/account/usage (see Usage & quota), and
either trim the request or upgrade your plan before retrying.
:::
{
"errors": [
{
"code": "over_quota",
"status": 402,
"detail": "Tracking these keywords would exceed your plan's keyword limit."
}
]
}
Managing tracked keywords later
Once keywords are tracked, you can:
- Pause or resume a keyword with
PATCH /v1/domains/{domain_uuid}/keywords/{uuid}and a body of{ "tracked": false }(ortrueto resume). Pausing stops new checks without deleting the keyword's history. - Stop tracking a keyword with
DELETE /v1/domains/{domain_uuid}/keywords/{uuid}, which soft-deletes the link. The call is idempotent — deleting an already-removed keyword still returns204.
See the Keywords reference for the full set of operations, and Bulk operations for tagging many keywords at once.
Step 3 — Read the latest rankings
To read back your keywords and their rankings, call
GET /v1/domains/{domain_uuid}/keywords:
curl "https://api.ranktracker.com/v1/domains/3f2a9c8e-1d4b-4a7f-9e2c-8b1a2c3d4e5f/keywords?per_page=100" \
-H "Authorization: tkn_usr_your_api_key_here"
import requests
domain_uuid = "3f2a9c8e-1d4b-4a7f-9e2c-8b1a2c3d4e5f"
resp = requests.get(
f"https://api.ranktracker.com/v1/domains/{domain_uuid}/keywords",
headers={"Authorization": "tkn_usr_your_api_key_here"},
params={"per_page": 100},
)
resp.raise_for_status()
for kw in resp.json()["data"]:
latest = (kw["attributes"]["results"]["history"] or [None])[0]
pos = latest["organic_current_position"] if latest else "no data yet"
print(kw["attributes"]["word"], "→", pos)
Each entry carries the keyword's setup (word, searchEngine, device,
location, language) plus a results object. Inside results:
summary— headline metrics for the keyword such assearch_volume,traffic,cost_per_click,competition, anddifficulty.history— an array of ranking snapshots over time. The most recent entry holds the current standing:organic_current_position,organic_day_change,organic_week_change,organic_best_position, the ranking URL, and more.
{
"data": [
{
"id": "a1b2c3d4-0000-4a7f-9e2c-111122223333",
"type": "keyword",
"attributes": {
"word": "rank tracker",
"searchEngine": { "uuid": "…", "name": "google" },
"device": { "uuid": "…", "name": "desktop" },
"location": { "uuid": "…", "code": "US", "name": "United States" },
"results": {
"summary": {
"search_volume": 8100,
"traffic": 240,
"difficulty": 42
},
"history": [
{
"created_at": "2026-07-03",
"organic_url": "https://www.example.com/",
"organic_current_position": 6,
"organic_best_position": 4,
"organic_day_change": 1,
"organic_week_change": -2
}
]
},
"tags": [],
"updatedAt": "2026-07-03T09:32:10Z"
}
}
]
}
:::note Fresh keywords have no data yet
A keyword you just added won't have rankings until the next scheduled check
runs. Until then results.history is an empty array. Poll this endpoint (or the
single-keyword GET /v1/domains/{domain_uuid}/keywords/{uuid}) after the first
check to pick up positions.
:::
Filtering and paginating the list
This list endpoint accepts page and per_page (max 1000), and returns the
totals in response headers — X-Total-Count, X-Total-Pages, X-Per-Page, and
X-Current-Page — so you can page through a large keyword set. You can also
filter by the ranking URL with results_organic_url to find which keywords rank
for a specific page. See the Pagination guide for the
paging pattern.
What's next
You now have a domain, a set of tracked keywords, and a way to read their rankings. From here:
- Add competitors — track competitor domains against yours to compare rankings side by side.
- Organise with tags — tag keywords individually
or bulk-tag many at once via
POST /v1/domains/{domain_uuid}/keywords/taggings. - Read SERP data — pull the full search-results page and cached SERP HTML for any tracked keyword.
- Ranking history & share of voice — the domain-level
GET /v1/domains/{uuid}/historyandGET /v1/domains/{uuid}/share-of-voiceendpoints return time series (both takestart_date/end_dateasYYYY-MM-DD, adeviceofall,desktop, ormobile, and a max range of 365 days). - Usage & quota — keep an eye on your keyword and
data-row limits with
GET /v1/account/usage.