Skip to main content

Bulk-tag keywords across domains

If you manage rank tracking for many clients, tags are how you keep thousands of keywords organised. This tutorial walks through a common agency workflow: create a tag once, then apply it to many keywords at once — across every domain you track — so you can slice reports by client, brand vs. non-brand, campaign, or priority.

You'll use two endpoints:

  • POST /v1/tags — create a tag (its name and colour).
  • POST /v1/domains/{domain_uuid}/keywords/taggings — apply one tag to many keywords in a single call (bulk-tag).

:::note What you'll need An API key with API access enabled on your plan, and the UUIDs of the domains you want to organise. Every example uses the production base URL https://api.ranktracker.com; all endpoints live under /v1. Send your key in the Authorization header with no Bearer prefix. New to the API? Start with the Quickstart. :::

How tagging works

A tag is an account-level resource: create it once and it's available for any keyword on any of your domains. Applying a tag to a keyword creates a tagging — a link between the tag and one keyword on one domain. The bulk endpoint creates many of those links in a single request.

Tags are created and applied with different keys depending on direction, in line with the API's convention: request bodies use snake_case, responses use camelCase. See Core concepts for the full model.

1. Create a tag

Create the tag you want to apply. Both name and colour are required — the colour is a free-form string (a hex value is conventional, but only its presence is validated).

curl -X POST https://api.ranktracker.com/v1/tags \
-H "Authorization: tkn_usr_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"tag": {
"name": "Brand",
"colour": "#4f46e5"
}
}'

A 201 Created returns the new tag. Keep the id — it's the tag_uuid you'll pass when bulk-tagging:

{
"data": {
"id": "7c1e9a2b-3f4d-4a8c-9b0e-1d2c3e4f5a6b",
"type": "keyword_tag",
"attributes": {
"name": "Brand",
"colour": "#4f46e5",
"keywordCount": 0,
"createdAt": "2026-07-02T10:15:33Z",
"updatedAt": "2026-07-02T10:15:33Z"
}
}
}

:::note Validation colour is required: omit it and you get a 422 Unprocessable Entity. Send an empty body with no tag object at all and you get a 400 Bad Request. See the Errors guide for the error envelope. :::

Already have your tags? List them with GET /v1/tags to find the id of an existing one instead of creating a new one:

curl https://api.ranktracker.com/v1/tags \
-H "Authorization: tkn_usr_your_api_key_here"

2. Find the keywords to tag

Bulk-tagging takes a list of keyword UUIDs on a specific domain. List a domain's keywords to collect the ones you want. The response includes each keyword's word and its current tags, so you can decide what to include:

curl "https://api.ranktracker.com/v1/domains/DOMAIN_UUID/keywords?per_page=1000" \
-H "Authorization: tkn_usr_your_api_key_here"
{
"data": [
{
"id": "a1b2c3d4-0000-4a00-9000-000000000001",
"type": "keyword",
"attributes": {
"word": "acme crm",
"tags": [],
"updatedAt": "2026-07-01T22:05:44Z"
}
},
{
"id": "a1b2c3d4-0000-4a00-9000-000000000002",
"type": "keyword",
"attributes": {
"word": "acme pricing",
"tags": [],
"updatedAt": "2026-07-01T22:05:44Z"
}
}
]
}

The keyword list is paginated. Pass page and per_page (max 1000) and read the X-Total-Pages / X-Total-Count response headers to know when you've seen them all. See the Pagination guide for the full loop.

:::tip Deciding what's "brand" The API doesn't classify keywords for you. A common approach is to filter the word client-side — e.g. every keyword containing the client's brand name goes into the Brand tag — then collect those ids for the bulk call below. :::

3. Bulk-tag the keywords

Apply the tag to all the collected keywords in one request. Send the tag_uuid and the array of keyword_uuids to the domain's taggings endpoint:

curl -X POST https://api.ranktracker.com/v1/domains/DOMAIN_UUID/keywords/taggings \
-H "Authorization: tkn_usr_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"tag_uuid": "7c1e9a2b-3f4d-4a8c-9b0e-1d2c3e4f5a6b",
"keyword_uuids": [
"a1b2c3d4-0000-4a00-9000-000000000001",
"a1b2c3d4-0000-4a00-9000-000000000002"
]
}'

A 201 Created returns the taggings that were created — one entry per newly-linked keyword:

{
"data": [
{
"id": "f0e1d2c3-1111-4b22-8c33-445566778899",
"type": "keyword_tag_link",
"attributes": {
"domainUuid": "DOMAIN_UUID",
"keywordUuid": "a1b2c3d4-0000-4a00-9000-000000000001",
"tagUuid": "7c1e9a2b-3f4d-4a8c-9b0e-1d2c3e4f5a6b",
"createdAt": "2026-07-02T10:20:01Z",
"updatedAt": "2026-07-02T10:20:01Z"
}
},
{
"id": "f0e1d2c3-2222-4b22-8c33-445566778899",
"type": "keyword_tag_link",
"attributes": {
"domainUuid": "DOMAIN_UUID",
"keywordUuid": "a1b2c3d4-0000-4a00-9000-000000000002",
"tagUuid": "7c1e9a2b-3f4d-4a8c-9b0e-1d2c3e4f5a6b",
"createdAt": "2026-07-02T10:20:01Z",
"updatedAt": "2026-07-02T10:20:01Z"
}
}
]
}

The operation is safe to re-run: it only creates taggings that don't already exist, so keywords that already carry the tag are skipped rather than duplicated.

:::note Errors to handle

  • 422keyword_uuids was empty. Always send at least one UUID.
  • 404 — one or more keyword UUIDs don't exist on that domain (the response lists the missing UUIDs). This is easy to trip when a keyword UUID is paired with the wrong domain — the pair must match.
  • 403 — your plan doesn't have API access enabled, or the domain isn't in your account.

See the Errors & rate limits guide for the full list. :::

4. Iterate across domains

Because tags are account-wide, the same tag_uuid works on every domain — but the taggings endpoint is scoped to one domain, so you loop: for each domain, list its keywords, pick the ones to tag, and post one bulk request. Tagging every keyword for a client, across all of that client's domains, looks like this:

import requests

BASE = "https://api.ranktracker.com/v1"
HEADERS = {"Authorization": "tkn_usr_your_api_key_here"}

TAG_UUID = "7c1e9a2b-3f4d-4a8c-9b0e-1d2c3e4f5a6b" # e.g. the client's tag
DOMAIN_UUIDS = [
"11111111-1111-4111-8111-111111111111",
"22222222-2222-4222-8222-222222222222",
]

def all_keyword_ids(domain_uuid):
ids, page = [], 1
while True:
resp = requests.get(
f"{BASE}/domains/{domain_uuid}/keywords",
headers=HEADERS,
params={"page": page, "per_page": 1000},
)
resp.raise_for_status()
rows = resp.json()["data"]
ids.extend(row["id"] for row in rows)
if page >= int(resp.headers.get("X-Total-Pages", 1)):
break
page += 1
return ids

for domain_uuid in DOMAIN_UUIDS:
keyword_ids = all_keyword_ids(domain_uuid)
if not keyword_ids:
continue # skip the empty POST — an empty list is a 422
resp = requests.post(
f"{BASE}/domains/{domain_uuid}/keywords/taggings",
headers=HEADERS,
json={"tag_uuid": TAG_UUID, "keyword_uuids": keyword_ids},
)
resp.raise_for_status()
print(f"{domain_uuid}: tagged {len(resp.json()['data'])} keyword(s)")

The same shape in Node.js:

const BASE = "https://api.ranktracker.com/v1";
const HEADERS = { Authorization: "tkn_usr_your_api_key_here" };

const TAG_UUID = "7c1e9a2b-3f4d-4a8c-9b0e-1d2c3e4f5a6b";
const DOMAIN_UUIDS = [
"11111111-1111-4111-8111-111111111111",
"22222222-2222-4222-8222-222222222222",
];

async function allKeywordIds(domainUuid) {
const ids = [];
let page = 1;
for (;;) {
const resp = await fetch(
`${BASE}/domains/${domainUuid}/keywords?page=${page}&per_page=1000`,
{ headers: HEADERS },
);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const body = await resp.json();
ids.push(...body.data.map((row) => row.id));
if (page >= Number(resp.headers.get("X-Total-Pages") ?? 1)) break;
page += 1;
}
return ids;
}

for (const domainUuid of DOMAIN_UUIDS) {
const keywordIds = await allKeywordIds(domainUuid);
if (keywordIds.length === 0) continue; // empty list is a 422
const resp = await fetch(
`${BASE}/domains/${domainUuid}/keywords/taggings`,
{
method: "POST",
headers: { ...HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ tag_uuid: TAG_UUID, keyword_uuids: keywordIds }),
},
);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const body = await resp.json();
console.log(`${domainUuid}: tagged ${body.data.length} keyword(s)`);
}

:::tip Batching very large domains keyword_uuids can hold many IDs, but for domains with tens of thousands of keywords it's kinder to your own retries to chunk the list — e.g. 1,000 UUIDs per bulk request. Because the call skips already-tagged keywords, chunks that overlap are harmless. Space large sequences of requests out; see Bulk operations for throughput guidance. :::

Untagging

There's no bulk untag endpoint yet. To remove a tag from keywords in bulk, untag them one at a time with the single-keyword endpoint:

curl -X DELETE \
https://api.ranktracker.com/v1/domains/DOMAIN_UUID/keywords/KEYWORD_UUID/tags/TAG_UUID \
-H "Authorization: tkn_usr_your_api_key_here"

A 204 No Content confirms the tagging was removed. Loop over the keyword UUIDs you want to clear, one DELETE each. (The same single-keyword endpoint tags one keyword when you POST to it — handy when you only need to adjust one.)

:::warning Bulk untag is not available Deleting a tag entirely with DELETE /v1/tags/{uuid} removes it everywhere, but there is no endpoint to remove one tag from many keywords at once. Until one ships, iterate the single-keyword DELETE above. :::

Next steps