Skip to main content

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:

FieldDescription
hostThe hostname to track, e.g. www.example.com.
schemehttps or http.
match_typeHow a result is matched to your domain, e.g. any, domain, or url.
project_nameA human-friendly label for the project.
gmb_nameGoogle Business Profile name for local tracking (send an empty string if unused).
colourA 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:

FieldDescription
wordsAn array of search terms to track, e.g. ["rank tracker", "seo tools"].
search_enginesAn array of engine configurations — each is a name + location + language + device combination.
frequencyHow often the keywords are checked, e.g. daily.

Each entry in search_engines describes one engine + location + language + device setup:

FieldDescription
nameThe search engine, e.g. google.
locationThe location to search from, e.g. United States.
languageThe results language, e.g. en.
devicedesktop 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 } (or true to 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 returns 204.

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 as search_volume, traffic, cost_per_click, competition, and difficulty.
  • 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}/history and GET /v1/domains/{uuid}/share-of-voice endpoints return time series (both take start_date / end_date as YYYY-MM-DD, a device of all, desktop, or mobile, and a max range of 365 days).
  • Usage & quota — keep an eye on your keyword and data-row limits with GET /v1/account/usage.