Skip to main content

Bulk operations

If you run an agency or a large account, you don't want to fire one HTTP request per keyword. This guide shows the two places the Ranktracker API lets you work in bulk:

It also covers the honest limits: there is no bulk-untag endpoint today, so untagging is one call per keyword.

:::tip New to the API? Start with the Quickstart and Authentication guides. This one assumes you already have an API key and can make an authenticated request. Tags and keywords are explained in Core concepts. :::

Bulk-tag keywords

The single-keyword way to tag is one call per (keyword, tag) pair:

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

That's fine for a keyword or two. To tag hundreds, use the bulk-tag endpoint, which applies one tag to many keywords in a single request:

POST /v1/domains/{domain_uuid}/keywords/taggings

The body takes the tag UUID and an array of keyword UUIDs — all scoped to the domain in the path:

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": "b3f1c2d4-…",
"keyword_uuids": [
"1a2b3c4d-…",
"5e6f7a8b-…",
"9c0d1e2f-…"
]
}'
import requests

domain_uuid = "DOMAIN_UUID"
resp = requests.post(
f"https://api.ranktracker.com/v1/domains/{domain_uuid}/keywords/taggings",
headers={"Authorization": "tkn_usr_your_api_key_here"},
json={
"tag_uuid": "b3f1c2d4-…",
"keyword_uuids": ["1a2b3c4d-…", "5e6f7a8b-…", "9c0d1e2f-…"],
},
)
resp.raise_for_status()
created = resp.json()["data"]
print(f"Created {len(created)} taggings")
const domainUuid = "DOMAIN_UUID";
const resp = await fetch(
`https://api.ranktracker.com/v1/domains/${domainUuid}/keywords/taggings`,
{
method: "POST",
headers: {
Authorization: "tkn_usr_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
tag_uuid: "b3f1c2d4-…",
keyword_uuids: ["1a2b3c4d-…", "5e6f7a8b-…", "9c0d1e2f-…"],
}),
},
);
const { data } = await resp.json();
console.log(`Created ${data.length} taggings`);

The response

On success the endpoint returns 201 Created with a JSON:API list under data. Each item is a tagging — the join between one keyword and the tag:

{
"data": [
{
"id": "7d8e9f0a-…",
"type": "keywordTag",
"attributes": {
"domainUuid": "DOMAIN_UUID",
"keywordUuid": "1a2b3c4d-…",
"tagUuid": "b3f1c2d4-…",
"createdAt": "2026-07-03T09:14:22Z",
"updatedAt": "2026-07-03T09:14:22Z"
}
},
{
"id": "8e9f0a1b-…",
"type": "keywordTag",
"attributes": {
"domainUuid": "DOMAIN_UUID",
"keywordUuid": "5e6f7a8b-…",
"tagUuid": "b3f1c2d4-…",
"createdAt": "2026-07-03T09:14:22Z",
"updatedAt": "2026-07-03T09:14:22Z"
}
}
]
}

:::note Already-tagged keywords are skipped, not duplicated The call only creates taggings that don't already exist. If some of the keywords you send already carry that tag, those are left as-is and only the new taggings come back in data. That makes the request safe to retry. :::

You need the tag UUID and the keyword UUIDs before you call this. Get tag UUIDs from GET /v1/tags (or create one with POST /v1/tags), and keyword UUIDs from GET /v1/domains/{domain_uuid}/keywords — the id of each keyword in the list.

Errors

The bulk-tag endpoint follows the standard JSON:API error envelope:

StatusWhen
422keyword_uuids is empty.
404One or more keyword UUIDs don't exist in this domain — the error detail lists the missing UUIDs.
403Your plan has API access disabled, or the tag/domain isn't yours.

:::warning 404 means nothing changed A 404 for missing keyword UUIDs is all-or-nothing — the call reports the unknown UUIDs and applies no taggings. Fix the list (or drop the bad UUIDs) and retry, rather than assuming a partial write happened. :::

Bulk-tag vs. single tag/untag

There are three tag operations. Only tagging has a bulk form:

OperationEndpointBulk?
Tag one keywordPOST /v1/domains/{d}/keywords/{k}/tags/{t}No
Untag one keywordDELETE /v1/domains/{d}/keywords/{k}/tags/{t}No
Tag many keywordsPOST /v1/domains/{d}/keywords/taggingsYes

The single-keyword POST returns 201 when it creates the tagging, or 200 if that keyword already had the tag (so it's idempotent too). The single-keyword DELETE returns 204.

There is no bulk-untag endpoint

Being upfront: a bulk-untag endpoint is not currently available. To remove a tag from many keywords, call the single-keyword DELETE once per keyword:

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"
import requests

domain_uuid = "DOMAIN_UUID"
tag_uuid = "b3f1c2d4-…"
keyword_uuids = ["1a2b3c4d-…", "5e6f7a8b-…", "9c0d1e2f-…"]

session = requests.Session()
session.headers["Authorization"] = "tkn_usr_your_api_key_here"

for keyword_uuid in keyword_uuids:
resp = session.delete(
f"https://api.ranktracker.com/v1/domains/{domain_uuid}"
f"/keywords/{keyword_uuid}/tags/{tag_uuid}"
)
# 204 = removed, 404 = the keyword didn't have that tag (safe to ignore)
if resp.status_code not in (204, 404):
resp.raise_for_status()

:::tip Loop gently When you're untagging in a loop, keep concurrency modest and back off on 503 (see rate limits). Deleting a tagging that isn't there returns 404, which you can safely treat as "already done." :::

Track many keywords efficiently

The other place bulk really pays off is adding keywords. A single POST /v1/domains/{domain_uuid}/keywords tracks the cartesian product of words × search_engines — so N words across M engine/location/device configurations creates N × M tracked keywords in one request. Batch your list instead of looping one word at a time.

curl -X POST \
https://api.ranktracker.com/v1/domains/DOMAIN_UUID/keywords \
-H "Authorization: tkn_usr_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"frequency": "daily",
"words": [
"rank tracker",
"keyword tracking",
"serp checker"
],
"search_engines": [
{
"name": "google",
"location": "United States",
"language": "en",
"device": "desktop"
},
{
"name": "google",
"location": "United States",
"language": "en",
"device": "mobile"
}
]
}'

The example above is 3 words × 2 engine configs = 6 tracked keywords from one call. The response is a 201 with a JSON:API list under data — one entry per keyword that was linked.

:::warning Watch your quota Every keyword created this way counts against your plan's keyword and data-row limits. If a batch would push you over, the whole call returns 402 and nothing is linked to the domain — it's all-or-nothing, so trim the batch and retry. Check headroom first with GET /v1/account/usage and size your batches to the remaining count. :::

Practical batching tips

  • Compose the matrix, don't loop words. Put every word in words and every engine/location/device combo in search_engines. One request beats N.
  • Check quota before a big batch. Read usage & quota and keep each batch at or under keywords.remaining so you don't trip a 402.
  • Tag right after you create. The create response gives you each new keyword's id. Collect those IDs and pass them straight into POST /keywords/taggings to label the whole batch in a second call.
  • Read results back with pagination. When you list keywords afterwards, page through with page and per_page (max 1000) and use the X-Total-Count / X-Total-Pages headers — see Pagination.
  • Back off on 503. Bulk writes are the most likely to be throttled. Throttled requests return 503 (there are no RateLimit-* headers yet), so retry with exponential backoff — details in Errors & rate limits.

Putting it together

A typical agency onboarding flow for a new domain:

  1. GET /v1/account/usage — confirm you have keyword headroom.
  2. POST /v1/domains/{domain_uuid}/keywords — add the whole words × search_engines matrix in one call; keep each id from the response.
  3. POST /v1/tags (once) — create the tag you'll group them under, if it doesn't exist yet.
  4. POST /v1/domains/{domain_uuid}/keywords/taggings — bulk-tag every new keyword UUID with that tag in one call.

Two or three requests, not hundreds.